/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.solr.schema;

import java.io.IOException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.DoublePoint;
import org.apache.lucene.document.FloatPoint;
import org.apache.lucene.document.IntPoint;
import org.apache.lucene.document.LongPoint;
import org.apache.lucene.document.NumericDocValuesField;
import org.apache.lucene.document.SortedNumericDocValuesField;
import org.apache.lucene.document.StoredField;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.DocValues;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexableField;
import org.apache.lucene.index.LeafReader;
import org.apache.lucene.index.LeafReaderContext;
import org.apache.lucene.index.PointValues;
import org.apache.lucene.index.StoredFields;
import org.apache.lucene.search.DocIdSetIterator;
import org.apache.lucene.search.IndexOrDocValuesQuery;
import org.apache.lucene.search.PointRangeQuery;
import org.apache.lucene.search.Query;
import org.apache.solr.SolrTestCaseJ4;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrInputDocument;
import org.apache.solr.common.params.CommonParams;
import org.apache.solr.index.SlowCompositeReaderWrapper;
import org.apache.solr.schema.IndexSchema.DynamicField;
import org.apache.solr.search.SolrQueryParser;
import org.apache.solr.util.DateMathParser;
import org.junit.After;
import org.junit.BeforeClass;
import org.junit.Test;

/** Tests for PointField functionality */
public class TestPointFields extends SolrTestCaseJ4 {

  // long overflow can occur in some date calculations if gaps are too large, so we limit to a
  // million years BC & AD.
  private static final long MIN_DATE_EPOCH_MILLIS =
      LocalDateTime.parse("-1000000-01-01T00:00:00")
          .toInstant(ZoneOffset.ofHours(0))
          .toEpochMilli();
  private static final long MAX_DATE_EPOCH_MILLIS =
      LocalDateTime.parse("+1000000-01-01T00:00:00")
          .toInstant(ZoneOffset.ofHours(0))
          .toEpochMilli();

  private static final String[] FIELD_SUFFIXES =
      new String[] {
        "",
        "_dv",
        "_mv",
        "_mv_dv",
        "_ni",
        "_ni_dv",
        "_ni_dv_ns",
        "_ni_dv_ns_mv",
        "_ni_mv",
        "_ni_mv_dv",
        "_ni_ns",
        "_ni_ns_mv",
        "_dv_ns",
        "_ni_ns_dv",
        "_dv_ns_mv",
        "_smf",
        "_dv_smf",
        "_mv_smf",
        "_mv_dv_smf",
        "_ni_dv_smf",
        "_ni_mv_dv_smf",
        "_sml",
        "_dv_sml",
        "_mv_sml",
        "_mv_dv_sml",
        "_ni_dv_sml",
        "_ni_mv_dv_sml"
      };

  @BeforeClass
  public static void beforeClass() throws Exception {
    initCore("solrconfig.xml", "schema-point.xml");
  }

  @Override
  @After
  public void tearDown() throws Exception {
    clearIndex();
    assertU(commit());
    super.tearDown();
  }

  @Test
  public void testIntPointFieldExactQuery() throws Exception {
    doTestIntPointFieldExactQuery("number_p_i", false);
    doTestIntPointFieldExactQuery("number_p_i_mv", false);
    doTestIntPointFieldExactQuery("number_p_i_dv", false);
    doTestIntPointFieldExactQuery("number_p_i_mv_dv", false);
    doTestIntPointFieldExactQuery("number_p_i_ni_dv", false);
    doTestIntPointFieldExactQuery("number_p_i_ni_ns_dv", false);
    doTestIntPointFieldExactQuery("number_p_i_ni_mv_dv", false);
  }

  @Test
  public void testIntPointFieldNonSearchableExactQuery() throws Exception {
    doTestIntPointFieldExactQuery("number_p_i_ni", false, false);
    doTestIntPointFieldExactQuery("number_p_i_ni_ns", false, false);
  }

  @Test
  public void testIntPointFieldReturn() throws Exception {
    int numValues = 10 * RANDOM_MULTIPLIER;
    String[] ints = toStringArray(getRandomInts(numValues, false));
    doTestPointFieldReturn("number_p_i", "int", ints);
    doTestPointFieldReturn("number_p_i_dv_ns", "int", ints);
    doTestPointFieldReturn("number_p_i_ni", "int", ints);
  }

  @Test
  public void testIntPointFieldRangeQuery() throws Exception {
    doTestIntPointFieldRangeQuery("number_p_i", "int", false);
    doTestIntPointFieldRangeQuery("number_p_i_ni_ns_dv", "int", false);
    doTestIntPointFieldRangeQuery("number_p_i_dv", "int", false);
  }

  @Test
  public void testIntPointFieldNonSearchableRangeQuery() throws Exception {
    doTestPointFieldNonSearchableRangeQuery(
        "number_p_i_ni", toStringArray(getRandomInts(1, false)));
    doTestPointFieldNonSearchableRangeQuery(
        "number_p_i_ni_ns", toStringArray(getRandomInts(1, false)));
    int numValues = 2 * RANDOM_MULTIPLIER;
    doTestPointFieldNonSearchableRangeQuery(
        "number_p_i_ni_ns_mv", toStringArray(getRandomInts(numValues, false)));
  }

  @Test
  public void testIntPointFieldSortAndFunction() throws Exception {

    final SortedSet<String> regexToTest = dynFieldRegexesForType(IntPointField.class);
    final List<String> sequential = Arrays.asList("0", "1", "2", "3", "4", "5", "6", "7", "8", "9");
    final List<Integer> randomInts = getRandomInts(10, false);
    final List<Integer> randomIntsMissing = getRandomInts(10, true);

    for (String r :
        Arrays.asList(
            "*_p_i",
            "*_p_i_dv",
            "*_p_i_dv_ns",
            "*_p_i_ni_dv",
            "*_p_i_ni_dv_ns",
            "*_p_i_ni_ns_dv")) {
      assertTrue(r, regexToTest.remove(r));
      String field = r.replace("*", "number");
      doTestPointFieldSort(field, sequential);
      doTestPointFieldSort(field, randomInts);
      doTestIntPointFunctionQuery(field);
    }
    for (String r :
        Arrays.asList(
            "*_p_i_smf",
            "*_p_i_dv_smf",
            "*_p_i_ni_dv_smf",
            "*_p_i_sml",
            "*_p_i_dv_sml",
            "*_p_i_ni_dv_sml")) {
      assertTrue(r, regexToTest.remove(r));
      String field = r.replace("*", "number");
      doTestPointFieldSort(field, sequential);
      doTestPointFieldSort(field, randomIntsMissing);
      doTestIntPointFunctionQuery(field);
    }

    // no docvalues
    for (String r : Arrays.asList("*_p_i_ni", "*_p_i_ni_ns")) {
      assertTrue(r, regexToTest.remove(r));
      String field = r.replace("*", "number");
      doTestPointFieldSortError(field, "w/o docValues", toStringArray(getRandomInts(1, false)));
      doTestPointFieldFunctionQueryError(
          field, "w/o docValues", toStringArray(getRandomInts(1, false)));
    }

    // multivalued, no docvalues
    for (String r :
        Arrays.asList(
            "*_p_i_mv", "*_p_i_ni_mv", "*_p_i_ni_ns_mv", "*_p_i_mv_smf", "*_p_i_mv_sml")) {

      assertTrue(r, regexToTest.remove(r));
      String field = r.replace("*", "number");
      doTestPointFieldSortError(field, "w/o docValues", toStringArray(getRandomInts(1, false)));
      int numValues = 2 * RANDOM_MULTIPLIER;
      doTestPointFieldSortError(
          field, "w/o docValues", toStringArray(getRandomInts(numValues, false)));
      doTestPointFieldFunctionQueryError(
          field, "multivalued", toStringArray(getRandomInts(1, false)));
      doTestPointFieldFunctionQueryError(
          field, "multivalued", toStringArray(getRandomInts(numValues, false)));
    }

    // multivalued, w/ docValues
    for (String r :
        Arrays.asList(
            "*_p_i_ni_mv_dv", "*_p_i_ni_dv_ns_mv",
            "*_p_i_dv_ns_mv", "*_p_i_mv_dv",
            "*_p_i_mv_dv_smf", "*_p_i_ni_mv_dv_smf",
            "*_p_i_mv_dv_sml", "*_p_i_ni_mv_dv_sml")) {
      assertTrue(r, regexToTest.remove(r));
      String field = r.replace("*", "number");

      // NOTE: only testing one value per doc here, but TestMinMaxOnMultiValuedField
      // covers this in more depth
      doTestPointFieldSort(field, sequential);
      doTestPointFieldSort(field, randomInts);

      // value source (w/o field(...,min|max)) usage should still error...
      int numValues = 2 * RANDOM_MULTIPLIER;
      doTestPointFieldFunctionQueryError(
          field, "multivalued", toStringArray(getRandomInts(1, false)));
      doTestPointFieldFunctionQueryError(
          field, "multivalued", toStringArray(getRandomInts(numValues, false)));
    }

    assertEquals("Missing types in the test", Collections.<String>emptySet(), regexToTest);
  }

  @Test
  public void testIntPointFieldFacetField() throws Exception {
    doTestPointFieldFacetField("number_p_i", "number_p_i_dv", getSequentialStringArrayWithInts(10));
    clearIndex();
    assertU(commit());
    doTestPointFieldFacetField(
        "number_p_i", "number_p_i_dv", toStringArray(getRandomInts(10, false)));
  }

  @Test
  public void testIntPointFieldRangeFacet() {
    String docValuesField = "number_p_i_dv";
    String nonDocValuesField = "number_p_i";
    int numValues = 10 * RANDOM_MULTIPLIER;
    int numBuckets = numValues / 2;
    List<Integer> values;
    List<Integer> sortedValues;
    int max;
    do {
      values = getRandomInts(numValues, false);
      sortedValues = values.stream().sorted().collect(Collectors.toList());
    } while ((max = sortedValues.get(sortedValues.size() - 1))
        >= Integer.MAX_VALUE - numValues); // leave room for rounding
    int min = sortedValues.get(0);
    int gap = (int) (((long) (max + numValues) - (long) min) / (long) numBuckets);
    int[] bucketCount = new int[numBuckets];
    int bucketNum = 0;
    int minBucketVal = min;
    for (Integer value : sortedValues) {
      while (((long) value - (long) minBucketVal) >= (long) gap) {
        ++bucketNum;
        minBucketVal += gap;
      }
      ++bucketCount[bucketNum];
    }

    for (int i = 0; i < numValues; i++) {
      assertU(
          adoc(
              "id",
              String.valueOf(i),
              docValuesField,
              String.valueOf(values.get(i)),
              nonDocValuesField,
              String.valueOf(values.get(i))));
    }
    assertU(commit());

    assertTrue(h.getCore().getLatestSchema().getField(docValuesField).hasDocValues());
    assertTrue(
        h.getCore().getLatestSchema().getField(docValuesField).getType() instanceof PointField);
    String[] testStrings = new String[numBuckets + 1];
    testStrings[numBuckets] = "//*[@numFound='" + numValues + "']";
    minBucketVal = min;
    for (int i = 0; i < numBuckets; minBucketVal += gap, ++i) {
      testStrings[i] =
          "//lst[@name='facet_counts']/lst[@name='facet_ranges']/lst[@name='"
              + docValuesField
              + "']/lst[@name='counts']/int[@name='"
              + minBucketVal
              + "'][.='"
              + bucketCount[i]
              + "']";
    }
    assertQ(
        req(
            "q",
            "*:*",
            "facet",
            "true",
            "facet.range",
            docValuesField,
            "facet.range.start",
            String.valueOf(min),
            "facet.range.end",
            String.valueOf(max),
            "facet.range.gap",
            String.valueOf(gap)),
        testStrings);
    assertQ(
        req(
            "q",
            "*:*",
            "facet",
            "true",
            "facet.range",
            docValuesField,
            "facet.range.start",
            String.valueOf(min),
            "facet.range.end",
            String.valueOf(max),
            "facet.range.gap",
            String.valueOf(gap),
            "facet.range.method",
            "dv"),
        testStrings);

    assertFalse(h.getCore().getLatestSchema().getField(nonDocValuesField).hasDocValues());
    assertTrue(
        h.getCore().getLatestSchema().getField(nonDocValuesField).getType() instanceof PointField);
    minBucketVal = min;
    for (int i = 0; i < numBuckets; minBucketVal += gap, ++i) {
      testStrings[i] =
          "//lst[@name='facet_counts']/lst[@name='facet_ranges']/lst[@name='"
              + nonDocValuesField
              + "']/lst[@name='counts']/int[@name='"
              + minBucketVal
              + "'][.='"
              + bucketCount[i]
              + "']";
    }
    // Range Faceting with method = filter should work
    assertQ(
        req(
            "q",
            "*:*",
            "facet",
            "true",
            "facet.range",
            nonDocValuesField,
            "facet.range.start",
            String.valueOf(min),
            "facet.range.end",
            String.valueOf(max),
            "facet.range.gap",
            String.valueOf(gap),
            "facet.range.method",
            "filter"),
        testStrings);
    // this should actually use filter method instead of dv
    assertQ(
        req(
            "q",
            "*:*",
            "facet",
            "true",
            "facet.range",
            nonDocValuesField,
            "facet.range.start",
            String.valueOf(min),
            "facet.range.end",
            String.valueOf(max),
            "facet.range.gap",
            String.valueOf(gap),
            "facet.range.method",
            "dv"),
        testStrings);
  }

  @Test
  public void testIntPointStats() {
    int numValues = 10 * RANDOM_MULTIPLIER;
    // don't produce numbers with exponents, since XPath comparison operators can't handle them
    List<Integer> values = getRandomInts(numValues, false, 9999999);
    // System.err.println(Arrays.toString(values.toArray(new Integer[values.size()])));
    List<Integer> sortedValues = values.stream().sorted().collect(Collectors.toList());
    double min = (double) sortedValues.get(0);
    double max = (double) sortedValues.get(sortedValues.size() - 1);

    String[] valArray = toStringArray(values);
    doTestPointStats("number_p_i", "number_p_i_dv", valArray, min, max, numValues, 1, 0D);
    doTestPointStats("number_p_i", "number_p_i_mv_dv", valArray, min, max, numValues, 1, 0D);
  }

  @Test
  public void testIntPointFieldMultiValuedExactQuery() throws Exception {
    String[] ints = toStringArray(getRandomInts(20, false));
    doTestPointFieldMultiValuedExactQuery("number_p_i_mv", ints);
    doTestPointFieldMultiValuedExactQuery("number_p_i_ni_mv_dv", ints);
  }

  @Test
  public void testIntPointFieldMultiValuedNonSearchableExactQuery() throws Exception {
    String[] ints = toStringArray(getRandomInts(20, false));
    doTestPointFieldMultiValuedExactQuery("number_p_i_ni_mv", ints, false);
    doTestPointFieldMultiValuedExactQuery("number_p_i_ni_ns_mv", ints, false);
  }

  @Test
  public void testIntPointFieldMultiValuedReturn() throws Exception {
    String[] ints = toStringArray(getRandomInts(20, false));
    doTestPointFieldMultiValuedReturn("number_p_i_mv", "int", ints);
    doTestPointFieldMultiValuedReturn("number_p_i_ni_mv_dv", "int", ints);
    doTestPointFieldMultiValuedReturn("number_p_i_dv_ns_mv", "int", ints);
  }

  @Test
  public void testIntPointFieldMultiValuedRangeQuery() throws Exception {
    String[] ints =
        toStringArray(getRandomInts(20, false).stream().sorted().collect(Collectors.toList()));
    doTestPointFieldMultiValuedRangeQuery("number_p_i_mv", "int", ints);
    doTestPointFieldMultiValuedRangeQuery("number_p_i_ni_mv_dv", "int", ints);
    doTestPointFieldMultiValuedRangeQuery("number_p_i_mv_dv", "int", ints);
  }

  @Test
  public void testIntPointFieldNotIndexed() throws Exception {
    String[] ints = toStringArray(getRandomInts(10, false));
    doTestFieldNotIndexed("number_p_i_ni", ints);
    doTestFieldNotIndexed("number_p_i_ni_mv", ints);
  }

  // TODO MV SORT?
  @Test
  public void testIntPointFieldMultiValuedFacetField() throws Exception {
    doTestPointFieldMultiValuedFacetField(
        "number_p_i_mv", "number_p_i_mv_dv", getSequentialStringArrayWithInts(20));
    String[] randomSortedInts =
        toStringArray(getRandomInts(20, false).stream().sorted().collect(Collectors.toList()));
    doTestPointFieldMultiValuedFacetField("number_p_i_mv", "number_p_i_mv_dv", randomSortedInts);
  }

  @Test
  public void testIntPointFieldMultiValuedRangeFacet() {
    String docValuesField = "number_p_i_mv_dv";
    String nonDocValuesField = "number_p_i_mv";
    int numValues = 20 * RANDOM_MULTIPLIER;
    int numBuckets = numValues / 2;
    List<Integer> values;
    List<PosVal<Integer>> sortedValues;
    int max;
    do {
      values = getRandomInts(numValues, false);
      sortedValues = toAscendingPosVals(values, true);
    } while ((max = sortedValues.get(sortedValues.size() - 1).val)
        >= Integer.MAX_VALUE - numValues); // leave room for rounding
    int min = sortedValues.get(0).val;
    int gap = (int) (((long) (max + numValues) - (long) min) / (long) numBuckets);
    List<Set<Integer>> docIdBucket = new ArrayList<>(numBuckets);
    for (int i = 0; i < numBuckets; ++i) {
      docIdBucket.add(new HashSet<>());
    }
    int bucketNum = 0;
    int minBucketVal = min;
    for (PosVal<Integer> value : sortedValues) {
      while ((long) value.val - (long) minBucketVal >= gap) {
        ++bucketNum;
        minBucketVal += gap;
      }
      docIdBucket.get(bucketNum).add(value.pos / 2); // each doc gets two consecutive values
    }
    for (int i = 0; i < numValues; i += 2) {
      assertU(
          adoc(
              "id",
              String.valueOf(i / 2),
              docValuesField,
              String.valueOf(values.get(i)),
              docValuesField,
              String.valueOf(values.get(i + 1)),
              nonDocValuesField,
              String.valueOf(values.get(i)),
              nonDocValuesField,
              String.valueOf(values.get(i + 1))));
    }
    assertU(commit());
    assertTrue(h.getCore().getLatestSchema().getField(docValuesField).hasDocValues());
    assertTrue(
        h.getCore().getLatestSchema().getField(docValuesField).getType() instanceof PointField);
    String[] testStrings = new String[numBuckets + 1];
    minBucketVal = min;
    testStrings[numBuckets] = "//*[@numFound='" + (numValues / 2) + "']";
    for (int i = 0; i < numBuckets; minBucketVal += gap, ++i) {
      testStrings[i] =
          "//lst[@name='facet_counts']/lst[@name='facet_ranges']/lst[@name='"
              + docValuesField
              + "']/lst[@name='counts']/int[@name='"
              + minBucketVal
              + "'][.='"
              + docIdBucket.get(i).size()
              + "']";
    }

    assertQ(
        req(
            "q",
            "*:*",
            "facet",
            "true",
            "facet.range",
            docValuesField,
            "facet.range.start",
            String.valueOf(min),
            "facet.range.end",
            String.valueOf(max),
            "facet.range.gap",
            String.valueOf(gap),
            "indent",
            "on"),
        testStrings);
    assertQ(
        req(
            "q",
            "*:*",
            "facet",
            "true",
            "facet.range",
            docValuesField,
            "facet.range.start",
            String.valueOf(min),
            "facet.range.end",
            String.valueOf(max),
            "facet.range.gap",
            String.valueOf(gap),
            "facet.range.method",
            "dv",
            "indent",
            "on"),
        testStrings);

    assertFalse(h.getCore().getLatestSchema().getField(nonDocValuesField).hasDocValues());
    assertTrue(
        h.getCore().getLatestSchema().getField(nonDocValuesField).getType() instanceof PointField);
    minBucketVal = min;
    for (int i = 0; i < numBuckets; minBucketVal += gap, ++i) {
      testStrings[i] =
          "//lst[@name='facet_counts']/lst[@name='facet_ranges']/lst[@name='"
              + nonDocValuesField
              + "']/lst[@name='counts']/int[@name='"
              + minBucketVal
              + "'][.='"
              + docIdBucket.get(i).size()
              + "']";
    }
    // Range Faceting with method = filter should work
    assertQ(
        req(
            "q",
            "*:*",
            "facet",
            "true",
            "facet.range",
            nonDocValuesField,
            "facet.range.start",
            String.valueOf(min),
            "facet.range.end",
            String.valueOf(max),
            "facet.range.gap",
            String.valueOf(gap),
            "facet.range.method",
            "filter",
            "indent",
            "on"),
        testStrings);
    // this should actually use filter method instead of dv
    assertQ(
        req(
            "q",
            "*:*",
            "facet",
            "true",
            "facet.range",
            nonDocValuesField,
            "facet.range.start",
            String.valueOf(min),
            "facet.range.end",
            String.valueOf(max),
            "facet.range.gap",
            String.valueOf(gap),
            "facet.range.method",
            "dv",
            "indent",
            "on"),
        testStrings);
  }

  @Test
  public void testIntPointMultiValuedFunctionQuery() throws Exception {
    doTestPointMultiValuedFunctionQuery(
        "number_p_i_mv", "number_p_i_mv_dv", getSequentialStringArrayWithInts(20));
    doTestPointMultiValuedFunctionQuery(
        "number_p_i_mv",
        "number_p_i_mv_dv",
        toStringArray(getRandomInts(20, false).stream().sorted().collect(Collectors.toList())));
  }

  @Test
  public void testIntPointFieldsAtomicUpdates() throws Exception {
    if (!Boolean.getBoolean("solr.index.updatelog.enabled")) {
      return;
    }
    doTestIntPointFieldsAtomicUpdates("number_p_i");
    doTestIntPointFieldsAtomicUpdates("number_p_i_dv");
    doTestIntPointFieldsAtomicUpdates("number_p_i_dv_ns");
  }

  @Test
  public void testMultiValuedIntPointFieldsAtomicUpdates() throws Exception {
    if (!Boolean.getBoolean("solr.index.updatelog.enabled")) {
      return;
    }
    String[] ints = toStringArray(getRandomInts(3, false));
    doTestMultiValuedPointFieldsAtomicUpdates("number_p_i_mv", "int", ints);
    doTestMultiValuedPointFieldsAtomicUpdates("number_p_i_ni_mv_dv", "int", ints);
    doTestMultiValuedPointFieldsAtomicUpdates("number_p_i_dv_ns_mv", "int", ints);
  }

  private <T> String[] toStringArray(List<T> list) {
    return list.stream()
        .map(String::valueOf)
        .collect(Collectors.toList())
        .toArray(new String[list.size()]);
  }

  private static class PosVal<T extends Comparable<T>> {
    int pos;
    T val;

    PosVal(int pos, T val) {
      this.pos = pos;
      this.val = val;
    }

    @Override
    public String toString() {
      return "(" + pos + ": " + val.toString() + ")";
    }
  }

  /**
   * Primary sort by value, with nulls either first or last as specified, and then secondary sort by
   * position.
   */
  private <T extends Comparable<T>> Comparator<PosVal<T>> getPosValComparator(
      final boolean ascending, final boolean nullsFirst) {
    return (o1, o2) -> {
      if (o1.val == null) {
        if (o2.val == null) {
          return ascending ? Integer.compare(o1.pos, o2.pos) : Integer.compare(o2.pos, o1.pos);
        } else {
          return nullsFirst ? -1 : 1;
        }
      } else if (o2.val == null) {
        return nullsFirst ? 1 : -1;
      } else {
        return ascending ? o1.val.compareTo(o2.val) : o2.val.compareTo(o1.val);
      }
    };
  }

  /**
   * Primary ascending sort by value, with missing values (represented as null) either first or last
   * as specified, and then secondary ascending sort by position.
   */
  private <T extends Comparable<T>> String[] toAscendingStringArray(
      List<T> list, boolean missingFirst) {
    return toStringArray(
        toAscendingPosVals(list, missingFirst).stream()
            .map(pv -> pv.val)
            .collect(Collectors.toList()));
  }

  /**
   * Primary ascending sort by value, with missing values (represented as null) either first or last
   * as specified, and then secondary ascending sort by position.
   *
   * @return a list of the (originally) positioned values sorted as described above.
   */
  private <T extends Comparable<T>> List<PosVal<T>> toAscendingPosVals(
      List<T> list, boolean missingFirst) {
    List<PosVal<T>> posVals =
        IntStream.range(0, list.size())
            .mapToObj(i -> new PosVal<>(i, list.get(i)))
            .collect(Collectors.toList());
    posVals.sort(getPosValComparator(true, missingFirst));
    return posVals;
  }

  /**
   * Primary descending sort by value, with missing values (represented as null) either first or
   * last as specified, and then secondary descending sort by position.
   *
   * @return a list of the (originally) positioned values sorted as described above.
   */
  private <T extends Comparable<T>> List<PosVal<T>> toDescendingPosVals(
      List<T> list, boolean missingFirst) {
    List<PosVal<T>> posVals =
        IntStream.range(0, list.size())
            .mapToObj(i -> new PosVal<>(i, list.get(i)))
            .collect(Collectors.toList());
    posVals.sort(getPosValComparator(false, missingFirst));
    return posVals;
  }

  @Test
  public void testIntPointSetQuery() {
    doTestSetQueries("number_p_i", toStringArray(getRandomInts(20, false)), false);
    doTestSetQueries("number_p_i_dv", toStringArray(getRandomInts(20, false)), false);
    doTestSetQueries("number_p_i_mv", toStringArray(getRandomInts(20, false)), true);
    doTestSetQueries("number_p_i_mv_dv", toStringArray(getRandomInts(20, false)), true);
    doTestSetQueries("number_p_i_ni_dv", toStringArray(getRandomInts(20, false)), false);
  }

  // DoublePointField

  @Test
  public void testDoublePointFieldExactQuery() throws Exception {
    doTestFloatPointFieldExactQuery("number_p_d", true);
    doTestFloatPointFieldExactQuery("number_p_d_mv", true);
    doTestFloatPointFieldExactQuery("number_p_d_dv", true);
    doTestFloatPointFieldExactQuery("number_p_d_mv_dv", true);
    doTestFloatPointFieldExactQuery("number_p_d_ni_dv", true);
    doTestFloatPointFieldExactQuery("number_p_d_ni_ns_dv", true);
    doTestFloatPointFieldExactQuery("number_p_d_ni_dv_ns", true);
    doTestFloatPointFieldExactQuery("number_p_d_ni_mv_dv", true);
  }

  @Test
  public void testDoublePointFieldNonSearchableExactQuery() throws Exception {
    doTestFloatPointFieldExactQuery("number_p_d_ni", false, true);
    doTestFloatPointFieldExactQuery("number_p_d_ni_ns", false, true);
  }

  @Test
  public void testDoublePointFieldReturn() throws Exception {
    int numValues = 10 * RANDOM_MULTIPLIER;
    String[] doubles = toStringArray(getRandomDoubles(numValues, false));
    doTestPointFieldReturn("number_p_d", "double", doubles);
    doTestPointFieldReturn("number_p_d_dv_ns", "double", doubles);
  }

  @Test
  public void testDoublePointFieldRangeQuery() throws Exception {
    doTestFloatPointFieldRangeQuery("number_p_d", "double", true);
    doTestFloatPointFieldRangeQuery("number_p_d_ni_ns_dv", "double", true);
    doTestFloatPointFieldRangeQuery("number_p_d_dv", "double", true);
  }

  @Test
  public void testDoubleFieldNonSearchableRangeQuery() throws Exception {
    doTestPointFieldNonSearchableRangeQuery(
        "number_p_d_ni", toStringArray(getRandomDoubles(1, false)));
    doTestPointFieldNonSearchableRangeQuery(
        "number_p_d_ni_ns", toStringArray(getRandomDoubles(1, false)));
    int numValues = 2 * RANDOM_MULTIPLIER;
    doTestPointFieldNonSearchableRangeQuery(
        "number_p_d_ni_ns_mv", toStringArray(getRandomDoubles(numValues, false)));
  }

  @Test
  public void testDoublePointFieldSortAndFunction() throws Exception {
    final SortedSet<String> regexToTest = dynFieldRegexesForType(DoublePointField.class);
    final List<String> sequential =
        Arrays.asList("0.0", "1.0", "2.0", "3.0", "4.0", "5.0", "6.0", "7.0", "8.0", "9.0");
    List<Double> randomDoubles = getRandomDoubles(10, false);
    List<Double> randomDoublesMissing = getRandomDoubles(10, true);

    for (String r :
        Arrays.asList(
            "*_p_d",
            "*_p_d_dv",
            "*_p_d_dv_ns",
            "*_p_d_ni_dv",
            "*_p_d_ni_dv_ns",
            "*_p_d_ni_ns_dv")) {
      assertTrue(r, regexToTest.remove(r));
      String field = r.replace("*", "number");
      doTestPointFieldSort(field, sequential);
      doTestPointFieldSort(field, randomDoubles);
      doTestDoublePointFunctionQuery(field);
    }

    for (String r :
        Arrays.asList(
            "*_p_d_smf",
            "*_p_d_dv_smf",
            "*_p_d_ni_dv_smf",
            "*_p_d_sml",
            "*_p_d_dv_sml",
            "*_p_d_ni_dv_sml")) {
      assertTrue(r, regexToTest.remove(r));
      String field = r.replace("*", "number");
      doTestPointFieldSort(field, sequential);
      doTestPointFieldSort(field, randomDoublesMissing);
      doTestDoublePointFunctionQuery(field);
    }

    for (String r : Arrays.asList("*_p_d_ni", "*_p_d_ni_ns")) {
      assertTrue(r, regexToTest.remove(r));
      String field = r.replace("*", "number");
      doTestPointFieldSortError(field, "w/o docValues", "42.34");
      doTestPointFieldFunctionQueryError(field, "w/o docValues", "42.34");
    }

    // multivalued, no docvalues
    for (String r :
        Arrays.asList(
            "*_p_d_mv", "*_p_d_ni_mv", "*_p_d_ni_ns_mv", "*_p_d_mv_smf", "*_p_d_mv_sml")) {

      assertTrue(r, regexToTest.remove(r));
      String field = r.replace("*", "number");
      doTestPointFieldSortError(field, "w/o docValues", "42.34");
      doTestPointFieldSortError(field, "w/o docValues", "42.34", "66.6");
      doTestPointFieldFunctionQueryError(field, "multivalued", "42.34");
      doTestPointFieldFunctionQueryError(field, "multivalued", "42.34", "66.6");
    }

    // multivalued, w/ docValues
    for (String r :
        Arrays.asList(
            "*_p_d_ni_mv_dv", "*_p_d_ni_dv_ns_mv",
            "*_p_d_dv_ns_mv", "*_p_d_mv_dv",
            "*_p_d_mv_dv_smf", "*_p_d_ni_mv_dv_smf",
            "*_p_d_mv_dv_sml", "*_p_d_ni_mv_dv_sml")) {
      assertTrue(r, regexToTest.remove(r));
      String field = r.replace("*", "number");

      // NOTE: only testing one value per doc here, but TestMinMaxOnMultiValuedField
      // covers this in more depth
      doTestPointFieldSort(field, sequential);
      doTestPointFieldSort(field, randomDoubles);

      // value source (w/o field(...,min|max)) usuage should still error...
      doTestPointFieldFunctionQueryError(field, "multivalued", "42.34");
      doTestPointFieldFunctionQueryError(field, "multivalued", "42.34", "66.6");
    }
    assertEquals("Missing types in the test", Collections.<String>emptySet(), regexToTest);
  }

  @Test
  public void testDoublePointFieldFacetField() throws Exception {
    doTestPointFieldFacetField(
        "number_p_d", "number_p_d_dv", getSequentialStringArrayWithDoubles(10));
    clearIndex();
    assertU(commit());
    doTestPointFieldFacetField(
        "number_p_d", "number_p_d_dv", toStringArray(getRandomDoubles(10, false)));
  }

  @Test
  public void testDoublePointFieldRangeFacet() {
    String docValuesField = "number_p_d_dv";
    String nonDocValuesField = "number_p_d";
    int numValues = 10 * RANDOM_MULTIPLIER;
    int numBuckets = numValues / 2;
    List<Double> values, sortedValues;
    double min, max, gap, buffer;
    do {
      values = getRandomDoubles(numValues, false);
      sortedValues = values.stream().sorted().collect(Collectors.toList());
      min = sortedValues.get(0);
      max = sortedValues.get(sortedValues.size() - 1);
      buffer =
          BigDecimal.valueOf(max)
              .subtract(BigDecimal.valueOf(min))
              .divide(BigDecimal.valueOf(numValues / 2), RoundingMode.HALF_UP)
              .doubleValue();
      gap =
          BigDecimal.valueOf(max)
              .subtract(BigDecimal.valueOf(min))
              .add(BigDecimal.valueOf(buffer * 2.0D))
              .divide(BigDecimal.valueOf(numBuckets), RoundingMode.HALF_UP)
              .doubleValue();
    } while (max >= Double.MAX_VALUE - buffer || min <= -Double.MAX_VALUE + buffer);
    // System.err.println("min: " + min + "   max: " + max + "   gap: " + gap + "   buffer: " +
    // buffer);
    int[] bucketCount = new int[numBuckets];
    int bucketNum = 0;
    double minBucketVal = min - buffer;
    // System.err.println("bucketNum: " + bucketNum + "   minBucketVal: " + minBucketVal);
    for (double value : sortedValues) {
      // System.err.println("value: " + value);
      while (value - minBucketVal >= gap) {
        ++bucketNum;
        minBucketVal += gap;
        // System.err.println("bucketNum: " + bucketNum + "   minBucketVal: " + minBucketVal);
      }
      ++bucketCount[bucketNum];
    }

    for (int i = 0; i < numValues; i++) {
      assertU(
          adoc(
              "id",
              String.valueOf(i),
              docValuesField,
              String.valueOf(values.get(i)),
              nonDocValuesField,
              String.valueOf(values.get(i))));
    }
    assertU(commit());

    String[] testStrings = new String[numBuckets + 1];
    testStrings[numBuckets] = "//*[@numFound='" + numValues + "']";
    minBucketVal = min - buffer;
    for (int i = 0; i < numBuckets; minBucketVal += gap, ++i) {
      testStrings[i] =
          "//lst[@name='facet_counts']/lst[@name='facet_ranges']/lst[@name='"
              + docValuesField
              + "']/lst[@name='counts']/int[@name='"
              + minBucketVal
              + "'][.='"
              + bucketCount[i]
              + "']";
    }
    assertQ(
        req(
            "q",
            "*:*",
            "facet",
            "true",
            "facet.range",
            docValuesField,
            "facet.range.start",
            String.valueOf(min - buffer),
            "facet.range.end",
            String.valueOf(max + buffer),
            "facet.range.gap",
            String.valueOf(gap)),
        testStrings);
    assertQ(
        req(
            "q",
            "*:*",
            "facet",
            "true",
            "facet.range",
            docValuesField,
            "facet.range.start",
            String.valueOf(min - buffer),
            "facet.range.end",
            String.valueOf(max + buffer),
            "facet.range.gap",
            String.valueOf(gap),
            "facet.range.method",
            "dv"),
        testStrings);

    minBucketVal = min - buffer;
    for (int i = 0; i < numBuckets; minBucketVal += gap, ++i) {
      testStrings[i] =
          "//lst[@name='facet_counts']/lst[@name='facet_ranges']/lst[@name='"
              + nonDocValuesField
              + "']/lst[@name='counts']/int[@name='"
              + minBucketVal
              + "'][.='"
              + bucketCount[i]
              + "']";
    }
    // Range Faceting with method = filter should work
    assertQ(
        req(
            "q",
            "*:*",
            "facet",
            "true",
            "facet.range",
            nonDocValuesField,
            "facet.range.start",
            String.valueOf(min - buffer),
            "facet.range.end",
            String.valueOf(max + buffer),
            "facet.range.gap",
            String.valueOf(gap),
            "facet.range.method",
            "filter"),
        testStrings);
    // this should actually use filter method instead of dv
    assertQ(
        req(
            "q",
            "*:*",
            "facet",
            "true",
            "facet.range",
            nonDocValuesField,
            "facet.range.start",
            String.valueOf(min - buffer),
            "facet.range.end",
            String.valueOf(max + buffer),
            "facet.range.gap",
            String.valueOf(gap),
            "facet.range.method",
            "dv"),
        testStrings);
  }

  @Test
  public void testDoublePointStats() {
    int numValues = 10 * RANDOM_MULTIPLIER;
    // don't produce numbers with exponents, since XPath comparison operators can't handle them: 7
    // digits of precision
    List<Float> values =
        getRandomInts(numValues, false, 9999999).stream()
            .map(v -> (float) ((double) v * Math.pow(10D, -1 * random().nextInt(8))))
            .collect(Collectors.toList());
    // System.err.println(Arrays.toString(values.toArray(new Float[values.size()])));
    List<Float> sortedValues = values.stream().sorted().collect(Collectors.toList());
    double min = (double) sortedValues.get(0);
    double max = (double) sortedValues.get(sortedValues.size() - 1);

    String[] valArray = toStringArray(values);
    doTestPointStats("number_p_d", "number_p_d_dv", valArray, min, max, numValues, 1, 1E-7D);
    doTestPointStats("number_p_d", "number_p_d_mv_dv", valArray, min, max, numValues, 1, 1E-7D);
  }

  @Test
  public void testDoublePointFieldMultiValuedExactQuery() throws Exception {
    String[] doubles = toStringArray(getRandomDoubles(20, false));
    doTestPointFieldMultiValuedExactQuery("number_p_d_mv", doubles);
    doTestPointFieldMultiValuedExactQuery("number_p_d_ni_mv_dv", doubles);
  }

  @Test
  public void testDoublePointFieldMultiValuedNonSearchableExactQuery() throws Exception {
    String[] doubles = toStringArray(getRandomDoubles(20, false));
    doTestPointFieldMultiValuedExactQuery("number_p_d_ni_mv", doubles, false);
    doTestPointFieldMultiValuedExactQuery("number_p_d_ni_ns_mv", doubles, false);
  }

  @Test
  public void testDoublePointFieldMultiValuedReturn() throws Exception {
    String[] doubles = toStringArray(getRandomDoubles(20, false));
    doTestPointFieldMultiValuedReturn("number_p_d_mv", "double", doubles);
    doTestPointFieldMultiValuedReturn("number_p_d_ni_mv_dv", "double", doubles);
    doTestPointFieldMultiValuedReturn("number_p_d_dv_ns_mv", "double", doubles);
  }

  @Test
  public void testDoublePointFieldMultiValuedRangeQuery() throws Exception {
    String[] doubles =
        toStringArray(getRandomDoubles(20, false).stream().sorted().collect(Collectors.toList()));
    doTestPointFieldMultiValuedRangeQuery("number_p_d_mv", "double", doubles);
    doTestPointFieldMultiValuedRangeQuery("number_p_d_ni_mv_dv", "double", doubles);
    doTestPointFieldMultiValuedRangeQuery("number_p_d_mv_dv", "double", doubles);
  }

  @Test
  public void testDoublePointFieldMultiValuedFacetField() throws Exception {
    doTestPointFieldMultiValuedFacetField(
        "number_p_d_mv", "number_p_d_mv_dv", getSequentialStringArrayWithDoubles(20));
    doTestPointFieldMultiValuedFacetField(
        "number_p_d_mv", "number_p_d_mv_dv", toStringArray(getRandomDoubles(20, false)));
  }

  @Test
  public void testDoublePointFieldMultiValuedRangeFacet() {
    String docValuesField = "number_p_d_mv_dv";
    SchemaField dvSchemaField = h.getCore().getLatestSchema().getField(docValuesField);
    assertTrue(dvSchemaField.multiValued());
    assertTrue(dvSchemaField.hasDocValues());
    assertTrue(dvSchemaField.getType() instanceof PointField);

    String nonDocValuesField = "number_p_d_mv";
    SchemaField nonDvSchemaField = h.getCore().getLatestSchema().getField(nonDocValuesField);
    assertTrue(nonDvSchemaField.multiValued());
    assertFalse(nonDvSchemaField.hasDocValues());
    assertTrue(nonDvSchemaField.getType() instanceof PointField);

    int numValues = 20 * RANDOM_MULTIPLIER;
    int numBuckets = numValues / 2;
    List<Double> values;
    List<PosVal<Double>> sortedValues;
    double min, max, gap, buffer;
    do {
      values = getRandomDoubles(numValues, false);
      sortedValues = toAscendingPosVals(values, true);
      min = sortedValues.get(0).val;
      max = sortedValues.get(sortedValues.size() - 1).val;
      buffer =
          BigDecimal.valueOf(max)
              .subtract(BigDecimal.valueOf(min))
              .divide(BigDecimal.valueOf(numValues / 2), RoundingMode.HALF_UP)
              .doubleValue();
      gap =
          BigDecimal.valueOf(max)
              .subtract(BigDecimal.valueOf(min))
              .add(BigDecimal.valueOf(buffer * 2.0D))
              .divide(BigDecimal.valueOf(numBuckets), RoundingMode.HALF_UP)
              .doubleValue();
    } while (max >= Double.MAX_VALUE - buffer || min <= -Double.MAX_VALUE + buffer);
    // System.err.println("min: " + min + "   max: " + max + "   gap: " + gap + "   buffer: " +
    // buffer);
    List<Set<Integer>> docIdBucket = new ArrayList<>(numBuckets);
    for (int i = 0; i < numBuckets; ++i) {
      docIdBucket.add(new HashSet<>());
    }
    int bucketNum = 0;
    double minBucketVal = min - buffer;
    // System.err.println("bucketNum: " + bucketNum + "   minBucketVal: " + minBucketVal);
    for (PosVal<Double> value : sortedValues) {
      // System.err.println("value.val: " + value.val);
      while (value.val - minBucketVal >= gap) {
        ++bucketNum;
        minBucketVal += gap;
        // System.err.println("bucketNum: " + bucketNum + "   minBucketVal: " + minBucketVal);
      }
      docIdBucket.get(bucketNum).add(value.pos / 2); // each doc gets two consecutive values
    }
    for (int i = 0; i < numValues; i += 2) {
      assertU(
          adoc(
              "id",
              String.valueOf(i / 2),
              docValuesField,
              String.valueOf(values.get(i)),
              docValuesField,
              String.valueOf(values.get(i + 1)),
              nonDocValuesField,
              String.valueOf(values.get(i)),
              nonDocValuesField,
              String.valueOf(values.get(i + 1))));
    }
    assertU(commit());

    String[] testStrings = new String[numBuckets + 1];
    testStrings[numBuckets] = "//*[@numFound='" + (numValues / 2) + "']";
    minBucketVal = min - buffer;
    for (int i = 0; i < numBuckets; minBucketVal += gap, ++i) {
      testStrings[i] =
          "//lst[@name='facet_counts']/lst[@name='facet_ranges']/lst[@name='"
              + docValuesField
              + "']/lst[@name='counts']/int[@name='"
              + minBucketVal
              + "'][.='"
              + docIdBucket.get(i).size()
              + "']";
    }
    assertQ(
        req(
            "q",
            "*:*",
            "facet",
            "true",
            "facet.range",
            docValuesField,
            "facet.range.start",
            String.valueOf(min - buffer),
            "facet.range.end",
            String.valueOf(max + buffer),
            "facet.range.gap",
            String.valueOf(gap),
            "indent",
            "on"),
        testStrings);
    assertQ(
        req(
            "q",
            "*:*",
            "facet",
            "true",
            "facet.range",
            docValuesField,
            "facet.range.start",
            String.valueOf(min - buffer),
            "facet.range.end",
            String.valueOf(max + buffer),
            "facet.range.gap",
            String.valueOf(gap),
            "facet.range.method",
            "dv",
            "indent",
            "on"),
        testStrings);

    minBucketVal = min - buffer;
    for (int i = 0; i < numBuckets; minBucketVal += gap, ++i) {
      testStrings[i] =
          "//lst[@name='facet_counts']/lst[@name='facet_ranges']/lst[@name='"
              + nonDocValuesField
              + "']/lst[@name='counts']/int[@name='"
              + minBucketVal
              + "'][.='"
              + docIdBucket.get(i).size()
              + "']";
    }
    // Range Faceting with method = filter should work
    assertQ(
        req(
            "q",
            "*:*",
            "facet",
            "true",
            "facet.range",
            nonDocValuesField,
            "facet.range.start",
            String.valueOf(min - buffer),
            "facet.range.end",
            String.valueOf(max + buffer),
            "facet.range.gap",
            String.valueOf(gap),
            "facet.range.method",
            "filter",
            "indent",
            "on"),
        testStrings);
    // this should actually use filter method instead of dv
    assertQ(
        req(
            "q",
            "*:*",
            "facet",
            "true",
            "facet.range",
            nonDocValuesField,
            "facet.range.start",
            String.valueOf(min - buffer),
            "facet.range.end",
            String.valueOf(max + buffer),
            "facet.range.gap",
            String.valueOf(gap),
            "facet.range.method",
            "dv",
            "indent",
            "on"),
        testStrings);
  }

  @Test
  public void testDoublePointMultiValuedFunctionQuery() throws Exception {
    doTestPointMultiValuedFunctionQuery(
        "number_p_d_mv", "number_p_d_mv_dv", getSequentialStringArrayWithDoubles(20));
    doTestPointMultiValuedFunctionQuery(
        "number_p_d_mv",
        "number_p_d_mv_dv",
        toAscendingStringArray(getRandomFloats(20, false), true));
  }

  @Test
  public void testDoublePointFieldsAtomicUpdates() throws Exception {
    if (!Boolean.getBoolean("solr.index.updatelog.enabled")) {
      return;
    }
    doTestDoublePointFieldsAtomicUpdates("number_p_d");
    doTestDoublePointFieldsAtomicUpdates("number_p_d_dv");
    doTestDoublePointFieldsAtomicUpdates("number_p_d_dv_ns");
  }

  @Test
  public void testMultiValuedDoublePointFieldsAtomicUpdates() throws Exception {
    if (!Boolean.getBoolean("solr.index.updatelog.enabled")) {
      return;
    }
    String[] doubles = toStringArray(getRandomDoubles(3, false));
    doTestMultiValuedPointFieldsAtomicUpdates("number_p_d_mv", "double", doubles);
    doTestMultiValuedPointFieldsAtomicUpdates("number_p_d_ni_mv_dv", "double", doubles);
    doTestMultiValuedPointFieldsAtomicUpdates("number_p_d_dv_ns_mv", "double", doubles);
  }

  @Test
  public void testDoublePointFieldNotIndexed() throws Exception {
    String[] doubles = toStringArray(getRandomDoubles(10, false));
    doTestFieldNotIndexed("number_p_d_ni", doubles);
    doTestFieldNotIndexed("number_p_d_ni_mv", doubles);
  }

  private void doTestFloatPointFieldsAtomicUpdates(String field) {
    float number1 = getRandomFloats(1, false).get(0);
    float number2;
    double inc1;
    for (; ; ) {
      number2 = getRandomFloats(1, false).get(0);
      inc1 = (double) number2 - (double) number1;
      if (Math.abs(inc1) < (double) Float.MAX_VALUE) {
        number2 = number1 + (float) inc1;
        break;
      }
    }
    assertU(adoc(sdoc("id", "1", field, String.valueOf(number1))));
    assertU(commit());

    assertU(adoc(sdoc("id", "1", field, Map.of("inc", (float) inc1))));
    assertU(commit());

    assertQ(req("q", "id:1"), "//result/doc[1]/float[@name='" + field + "'][.='" + number2 + "']");

    float number3 = getRandomFloats(1, false).get(0);
    assertU(adoc(sdoc("id", "1", field, Map.of("set", number3))));
    assertU(commit());

    assertQ(req("q", "id:1"), "//result/doc[1]/float[@name='" + field + "'][.='" + number3 + "']");
  }

  private void doTestDoublePointFieldsAtomicUpdates(String field) {
    double number1 = getRandomDoubles(1, false).get(0);
    double number2;
    BigDecimal inc1;
    for (; ; ) {
      number2 = getRandomDoubles(1, false).get(0);
      inc1 = BigDecimal.valueOf(number2).subtract(BigDecimal.valueOf(number1));
      if (inc1.abs().compareTo(BigDecimal.valueOf(Double.MAX_VALUE)) <= 0) {
        number2 = number1 + inc1.doubleValue();
        break;
      }
    }
    assertU(adoc(sdoc("id", "1", field, String.valueOf(number1))));
    assertU(commit());

    assertU(adoc(sdoc("id", "1", field, Map.of("inc", inc1.doubleValue()))));
    assertU(commit());

    assertQ(req("q", "id:1"), "//result/doc[1]/double[@name='" + field + "'][.='" + number2 + "']");

    double number3 = getRandomDoubles(1, false).get(0);
    assertU(adoc(sdoc("id", "1", field, Map.of("set", number3))));
    assertU(commit());

    assertQ(req("q", "id:1"), "//result/doc[1]/double[@name='" + field + "'][.='" + number3 + "']");
  }

  @Test
  public void testDoublePointSetQuery() {
    doTestSetQueries("number_p_d", toStringArray(getRandomDoubles(20, false)), false);
    doTestSetQueries("number_p_d_dv", toStringArray(getRandomDoubles(20, false)), false);
    doTestSetQueries("number_p_d_mv", toStringArray(getRandomDoubles(20, false)), true);
    doTestSetQueries("number_p_d_mv_dv", toStringArray(getRandomDoubles(20, false)), true);
    doTestSetQueries("number_p_d_ni_dv", toStringArray(getRandomDoubles(20, false)), false);
  }

  // Float

  @Test
  public void testFloatPointFieldExactQuery() throws Exception {
    doTestFloatPointFieldExactQuery("number_p_f", false);
    doTestFloatPointFieldExactQuery("number_p_f_mv", false);
    doTestFloatPointFieldExactQuery("number_p_f_dv", false);
    doTestFloatPointFieldExactQuery("number_p_f_mv_dv", false);
    doTestFloatPointFieldExactQuery("number_p_f_ni_dv", false);
    doTestFloatPointFieldExactQuery("number_p_f_ni_ns_dv", false);
    doTestFloatPointFieldExactQuery("number_p_f_ni_dv_ns", false);
    doTestFloatPointFieldExactQuery("number_p_f_ni_mv_dv", false);
  }

  @Test
  public void testFloatPointFieldNonSearchableExactQuery() throws Exception {
    doTestFloatPointFieldExactQuery("number_p_f_ni", false, false);
    doTestFloatPointFieldExactQuery("number_p_f_ni_ns", false, false);
  }

  @Test
  public void testFloatPointFieldReturn() throws Exception {
    int numValues = 10 * RANDOM_MULTIPLIER;
    String[] floats = toStringArray(getRandomFloats(numValues, false));
    doTestPointFieldReturn("number_p_f", "float", floats);
    doTestPointFieldReturn("number_p_f_dv_ns", "float", floats);
  }

  @Test
  public void testFloatPointFieldRangeQuery() throws Exception {
    doTestFloatPointFieldRangeQuery("number_p_f", "float", false);
    doTestFloatPointFieldRangeQuery("number_p_f_ni_ns_dv", "float", false);
    doTestFloatPointFieldRangeQuery("number_p_f_dv", "float", false);
  }

  @Test
  public void testFloatPointFieldNonSearchableRangeQuery() throws Exception {
    doTestPointFieldNonSearchableRangeQuery(
        "number_p_f_ni", toStringArray(getRandomFloats(1, false)));
    doTestPointFieldNonSearchableRangeQuery(
        "number_p_f_ni_ns", toStringArray(getRandomFloats(1, false)));
    int numValues = 2 * RANDOM_MULTIPLIER;
    doTestPointFieldNonSearchableRangeQuery(
        "number_p_f_ni_ns_mv", toStringArray(getRandomFloats(numValues, false)));
  }

  @Test
  public void testFloatPointFieldSortAndFunction() throws Exception {
    final SortedSet<String> regexToTest = dynFieldRegexesForType(FloatPointField.class);
    final List<String> sequential =
        Arrays.asList("0.0", "1.0", "2.0", "3.0", "4.0", "5.0", "6.0", "7.0", "8.0", "9.0");
    final List<Float> randomFloats = getRandomFloats(10, false);
    final List<Float> randomFloatsMissing = getRandomFloats(10, true);

    for (String r :
        Arrays.asList(
            "*_p_f",
            "*_p_f_dv",
            "*_p_f_dv_ns",
            "*_p_f_ni_dv",
            "*_p_f_ni_dv_ns",
            "*_p_f_ni_ns_dv")) {
      assertTrue(r, regexToTest.remove(r));
      String field = r.replace("*", "number");
      doTestPointFieldSort(field, sequential);
      doTestPointFieldSort(field, randomFloats);

      doTestFloatPointFunctionQuery(field);
    }
    for (String r :
        Arrays.asList(
            "*_p_f_smf",
            "*_p_f_dv_smf",
            "*_p_f_ni_dv_smf",
            "*_p_f_sml",
            "*_p_f_dv_sml",
            "*_p_f_ni_dv_sml")) {
      assertTrue(r, regexToTest.remove(r));
      String field = r.replace("*", "number");
      doTestPointFieldSort(field, sequential);
      doTestPointFieldSort(field, randomFloatsMissing);
      doTestFloatPointFunctionQuery(field);
    }

    for (String r : Arrays.asList("*_p_f_ni", "*_p_f_ni_ns")) {
      assertTrue(r, regexToTest.remove(r));
      String field = r.replace("*", "number");
      doTestPointFieldSortError(field, "w/o docValues", "42.34");
      doTestPointFieldFunctionQueryError(field, "w/o docValues", "42.34");
    }

    // multivalued, no docvalues
    for (String r :
        Arrays.asList(
            "*_p_f_mv", "*_p_f_ni_mv", "*_p_f_ni_ns_mv", "*_p_f_mv_smf", "*_p_f_mv_sml")) {

      assertTrue(r, regexToTest.remove(r));
      String field = r.replace("*", "number");
      doTestPointFieldSortError(field, "w/o docValues", "42.34");
      doTestPointFieldSortError(field, "w/o docValues", "42.34", "66.6");
      doTestPointFieldFunctionQueryError(field, "multivalued", "42.34");
      doTestPointFieldFunctionQueryError(field, "multivalued", "42.34", "66.6");
    }

    // multivalued, w/ docValues
    for (String r :
        Arrays.asList(
            "*_p_f_ni_mv_dv", "*_p_f_ni_dv_ns_mv",
            "*_p_f_dv_ns_mv", "*_p_f_mv_dv",
            "*_p_f_mv_dv_smf", "*_p_f_ni_mv_dv_smf",
            "*_p_f_mv_dv_sml", "*_p_f_ni_mv_dv_sml")) {
      assertTrue(r, regexToTest.remove(r));
      String field = r.replace("*", "number");

      // NOTE: only testing one value per doc here, but TestMinMaxOnMultiValuedField
      // covers this in more depth
      doTestPointFieldSort(field, sequential);
      doTestPointFieldSort(field, randomFloats);

      // value source (w/o field(...,min|max)) usage should still error...
      doTestPointFieldFunctionQueryError(field, "multivalued", "42.34");
      doTestPointFieldFunctionQueryError(field, "multivalued", "42.34", "66.6");
    }
    assertEquals("Missing types in the test", Collections.<String>emptySet(), regexToTest);
  }

  @Test
  public void testFloatPointFieldFacetField() throws Exception {
    doTestPointFieldFacetField(
        "number_p_f", "number_p_f_dv", getSequentialStringArrayWithDoubles(10));
    clearIndex();
    assertU(commit());
    doTestPointFieldFacetField(
        "number_p_f", "number_p_f_dv", toStringArray(getRandomFloats(10, false)));
  }

  @Test
  public void testFloatPointFieldRangeFacet() {
    String docValuesField = "number_p_f_dv";
    String nonDocValuesField = "number_p_f";
    int numValues = 10 * RANDOM_MULTIPLIER;
    int numBuckets = numValues / 2;
    List<Float> values, sortedValues;
    float min, max, gap, buffer;
    do {
      values = getRandomFloats(numValues, false);
      sortedValues = values.stream().sorted().collect(Collectors.toList());
      min = sortedValues.get(0);
      max = sortedValues.get(sortedValues.size() - 1);
      buffer = (float) (((double) max - (double) min) / (double) numValues / 2.0D);
      gap =
          (float)
              (((double) max + (double) buffer - (double) min + (double) buffer)
                  / (double) numBuckets);
    } while (max >= Float.MAX_VALUE - buffer || min <= -Float.MAX_VALUE + buffer);
    // System.err.println("min: " + min + "   max: " + max + "   gap: " + gap + "   buffer: " +
    // buffer);
    int[] bucketCount = new int[numBuckets];
    int bucketNum = 0;
    float minBucketVal = min - buffer;
    // System.err.println("bucketNum: " + bucketNum + "   minBucketVal: " + minBucketVal);
    for (float value : sortedValues) {
      // System.err.println("value: " + value);
      while (value - minBucketVal >= gap) {
        ++bucketNum;
        minBucketVal += gap;
        // System.err.println("bucketNum: " + bucketNum + "   minBucketVal: " + minBucketVal);
      }
      ++bucketCount[bucketNum];
    }

    for (int i = 0; i < numValues; i++) {
      assertU(
          adoc(
              "id",
              String.valueOf(i),
              docValuesField,
              String.valueOf(values.get(i)),
              nonDocValuesField,
              String.valueOf(values.get(i))));
    }
    assertU(commit());

    assertTrue(h.getCore().getLatestSchema().getField(docValuesField).hasDocValues());
    assertTrue(
        h.getCore().getLatestSchema().getField(docValuesField).getType() instanceof PointField);
    String[] testStrings = new String[numBuckets + 1];
    testStrings[numBuckets] = "//*[@numFound='" + numValues + "']";
    minBucketVal = min - buffer;
    for (int i = 0; i < numBuckets; minBucketVal += gap, ++i) {
      testStrings[i] =
          "//lst[@name='facet_counts']/lst[@name='facet_ranges']/lst[@name='"
              + docValuesField
              + "']/lst[@name='counts']/int[@name='"
              + minBucketVal
              + "'][.='"
              + bucketCount[i]
              + "']";
    }
    assertQ(
        req(
            "q",
            "*:*",
            "facet",
            "true",
            "facet.range",
            docValuesField,
            "facet.range.start",
            String.valueOf(min - buffer),
            "facet.range.end",
            String.valueOf(max + buffer),
            "facet.range.gap",
            String.valueOf(gap)),
        testStrings);
    assertQ(
        req(
            "q",
            "*:*",
            "facet",
            "true",
            "facet.range",
            docValuesField,
            "facet.range.start",
            String.valueOf(min - buffer),
            "facet.range.end",
            String.valueOf(max + buffer),
            "facet.range.gap",
            String.valueOf(gap),
            "facet.range.method",
            "dv"),
        testStrings);

    assertFalse(h.getCore().getLatestSchema().getField(nonDocValuesField).hasDocValues());
    assertTrue(
        h.getCore().getLatestSchema().getField(nonDocValuesField).getType() instanceof PointField);
    minBucketVal = min - buffer;
    for (int i = 0; i < numBuckets; minBucketVal += gap, ++i) {
      testStrings[i] =
          "//lst[@name='facet_counts']/lst[@name='facet_ranges']/lst[@name='"
              + nonDocValuesField
              + "']/lst[@name='counts']/int[@name='"
              + minBucketVal
              + "'][.='"
              + bucketCount[i]
              + "']";
    }
    // Range Faceting with method = filter should work
    assertQ(
        req(
            "q",
            "*:*",
            "facet",
            "true",
            "facet.range",
            nonDocValuesField,
            "facet.range.start",
            String.valueOf(min - buffer),
            "facet.range.end",
            String.valueOf(max + buffer),
            "facet.range.gap",
            String.valueOf(gap),
            "facet.range.method",
            "filter"),
        testStrings);
    // this should actually use filter method instead of dv
    assertQ(
        req(
            "q",
            "*:*",
            "facet",
            "true",
            "facet.range",
            nonDocValuesField,
            "facet.range.start",
            String.valueOf(min - buffer),
            "facet.range.end",
            String.valueOf(max + buffer),
            "facet.range.gap",
            String.valueOf(gap),
            "facet.range.method",
            "dv"),
        testStrings);
  }

  @Test
  public void testFloatPointStats() {
    int numValues = 10 * RANDOM_MULTIPLIER;
    // don't produce numbers with exponents, since XPath comparison operators can't handle them: 7
    // digits of precision
    List<Float> values =
        getRandomInts(numValues, false, 9999999).stream()
            .map(v -> (float) ((double) v * Math.pow(10D, -1 * random().nextInt(8))))
            .collect(Collectors.toList());
    // System.err.println(Arrays.toString(values.toArray(new Float[values.size()])));
    List<Float> sortedValues = values.stream().sorted().collect(Collectors.toList());
    double min = (double) sortedValues.get(0);
    double max = (double) sortedValues.get(sortedValues.size() - 1);

    String[] valArray = toStringArray(values);
    doTestPointStats("number_p_f", "number_p_f_dv", valArray, min, max, numValues, 1, 1E-7D);
    doTestPointStats("number_p_f", "number_p_f_mv_dv", valArray, min, max, numValues, 1, 1E-7D);
  }

  @Test
  public void testFloatPointFieldMultiValuedExactQuery() throws Exception {
    String[] floats = toStringArray(getRandomFloats(20, false));
    doTestPointFieldMultiValuedExactQuery("number_p_f_mv", floats);
    doTestPointFieldMultiValuedExactQuery("number_p_f_ni_mv_dv", floats);
  }

  @Test
  public void testFloatPointFieldMultiValuedNonSearchableExactQuery() throws Exception {
    String[] floats = toStringArray(getRandomFloats(20, false));
    doTestPointFieldMultiValuedExactQuery("number_p_f_ni_mv", floats, false);
    doTestPointFieldMultiValuedExactQuery("number_p_f_ni_ns_mv", floats, false);
  }

  @Test
  public void testFloatPointFieldMultiValuedReturn() throws Exception {
    String[] floats = toStringArray(getRandomFloats(20, false));
    doTestPointFieldMultiValuedReturn("number_p_f_mv", "float", floats);
    doTestPointFieldMultiValuedReturn("number_p_f_ni_mv_dv", "float", floats);
    doTestPointFieldMultiValuedReturn("number_p_f_dv_ns_mv", "float", floats);
  }

  @Test
  public void testFloatPointFieldMultiValuedRangeQuery() throws Exception {
    String[] floats =
        toStringArray(getRandomFloats(20, false).stream().sorted().collect(Collectors.toList()));
    doTestPointFieldMultiValuedRangeQuery("number_p_f_mv", "float", floats);
    doTestPointFieldMultiValuedRangeQuery("number_p_f_ni_mv_dv", "float", floats);
    doTestPointFieldMultiValuedRangeQuery("number_p_f_mv_dv", "float", floats);
  }

  @Test
  public void testFloatPointFieldMultiValuedRangeFacet() {
    String docValuesField = "number_p_f_mv_dv";
    SchemaField dvSchemaField = h.getCore().getLatestSchema().getField(docValuesField);
    assertTrue(dvSchemaField.multiValued());
    assertTrue(dvSchemaField.hasDocValues());
    assertTrue(dvSchemaField.getType() instanceof PointField);

    String nonDocValuesField = "number_p_f_mv";
    SchemaField nonDvSchemaField = h.getCore().getLatestSchema().getField(nonDocValuesField);
    assertTrue(nonDvSchemaField.multiValued());
    assertFalse(nonDvSchemaField.hasDocValues());
    assertTrue(nonDvSchemaField.getType() instanceof PointField);

    int numValues = 20 * RANDOM_MULTIPLIER;
    int numBuckets = numValues / 2;
    List<Float> values;
    List<PosVal<Float>> sortedValues;
    float min, max, gap, buffer;
    do {
      values = getRandomFloats(numValues, false);
      sortedValues = toAscendingPosVals(values, true);
      min = sortedValues.get(0).val;
      max = sortedValues.get(sortedValues.size() - 1).val;
      buffer = (float) (((double) max - (double) min) / (double) numValues / 2.0D);
      gap =
          (float)
              (((double) max + (double) buffer - (double) min + (double) buffer)
                  / (double) numBuckets);
    } while (max >= Float.MAX_VALUE - buffer || min <= -Float.MAX_VALUE + buffer);
    // System.err.println("min: " + min + "   max: " + max + "   gap: " + gap + "   buffer: " +
    // buffer);
    List<Set<Integer>> docIdBucket = new ArrayList<>(numBuckets);
    for (int i = 0; i < numBuckets; ++i) {
      docIdBucket.add(new HashSet<>());
    }
    int bucketNum = 0;
    float minBucketVal = min - buffer;
    // System.err.println("bucketNum: " + bucketNum + "   minBucketVal: " + minBucketVal);
    for (PosVal<Float> value : sortedValues) {
      // System.err.println("value.val: " + value.val);
      while (value.val - minBucketVal >= gap) {
        ++bucketNum;
        minBucketVal += gap;
        // System.err.println("bucketNum: " + bucketNum + "   minBucketVal: " + minBucketVal);
      }
      docIdBucket.get(bucketNum).add(value.pos / 2); // each doc gets two consecutive values
    }
    for (int i = 0; i < numValues; i += 2) {
      assertU(
          adoc(
              "id",
              String.valueOf(i / 2),
              docValuesField,
              String.valueOf(values.get(i)),
              docValuesField,
              String.valueOf(values.get(i + 1)),
              nonDocValuesField,
              String.valueOf(values.get(i)),
              nonDocValuesField,
              String.valueOf(values.get(i + 1))));
    }
    assertU(commit());
    assertTrue(h.getCore().getLatestSchema().getField(docValuesField).hasDocValues());
    assertTrue(
        h.getCore().getLatestSchema().getField(docValuesField).getType() instanceof PointField);
    String[] testStrings = new String[numBuckets + 1];
    minBucketVal = min - buffer;
    testStrings[numBuckets] = "//*[@numFound='" + (numValues / 2) + "']";
    for (int i = 0; i < numBuckets; minBucketVal += gap, ++i) {
      testStrings[i] =
          "//lst[@name='facet_counts']/lst[@name='facet_ranges']/lst[@name='"
              + docValuesField
              + "']/lst[@name='counts']/int[@name='"
              + minBucketVal
              + "'][.='"
              + docIdBucket.get(i).size()
              + "']";
    }

    assertQ(
        req(
            "q",
            "*:*",
            "facet",
            "true",
            "facet.range",
            docValuesField,
            "facet.range.start",
            String.valueOf(min - buffer),
            "facet.range.end",
            String.valueOf(max + buffer),
            "facet.range.gap",
            String.valueOf(gap),
            "indent",
            "on"),
        testStrings);
    assertQ(
        req(
            "q",
            "*:*",
            "facet",
            "true",
            "facet.range",
            docValuesField,
            "facet.range.start",
            String.valueOf(min - buffer),
            "facet.range.end",
            String.valueOf(max + buffer),
            "facet.range.gap",
            String.valueOf(gap),
            "facet.range.method",
            "dv",
            "indent",
            "on"),
        testStrings);

    assertFalse(h.getCore().getLatestSchema().getField(nonDocValuesField).hasDocValues());
    assertTrue(
        h.getCore().getLatestSchema().getField(nonDocValuesField).getType() instanceof PointField);
    minBucketVal = min - buffer;
    for (int i = 0; i < numBuckets; minBucketVal += gap, ++i) {
      testStrings[i] =
          "//lst[@name='facet_counts']/lst[@name='facet_ranges']/lst[@name='"
              + nonDocValuesField
              + "']/lst[@name='counts']/int[@name='"
              + minBucketVal
              + "'][.='"
              + docIdBucket.get(i).size()
              + "']";
    }
    // Range Faceting with method = filter should work
    assertQ(
        req(
            "q",
            "*:*",
            "facet",
            "true",
            "facet.range",
            nonDocValuesField,
            "facet.range.start",
            String.valueOf(min - buffer),
            "facet.range.end",
            String.valueOf(max + buffer),
            "facet.range.gap",
            String.valueOf(gap),
            "facet.range.method",
            "filter",
            "indent",
            "on"),
        testStrings);
    // this should actually use filter method instead of dv
    assertQ(
        req(
            "q",
            "*:*",
            "facet",
            "true",
            "facet.range",
            nonDocValuesField,
            "facet.range.start",
            String.valueOf(min - buffer),
            "facet.range.end",
            String.valueOf(max + buffer),
            "facet.range.gap",
            String.valueOf(gap),
            "facet.range.method",
            "dv",
            "indent",
            "on"),
        testStrings);
  }

  @Test
  public void testFloatPointFieldMultiValuedFacetField() throws Exception {
    doTestPointFieldMultiValuedFacetField(
        "number_p_f_mv", "number_p_f_mv_dv", getSequentialStringArrayWithDoubles(20));
    doTestPointFieldMultiValuedFacetField(
        "number_p_f_mv", "number_p_f_mv_dv", toStringArray(getRandomFloats(20, false)));
  }

  @Test
  public void testFloatPointMultiValuedFunctionQuery() throws Exception {
    doTestPointMultiValuedFunctionQuery(
        "number_p_f_mv", "number_p_f_mv_dv", getSequentialStringArrayWithDoubles(20));
    doTestPointMultiValuedFunctionQuery(
        "number_p_f_mv",
        "number_p_f_mv_dv",
        toAscendingStringArray(getRandomFloats(20, false), true));
  }

  @Test
  public void testFloatPointFieldsAtomicUpdates() throws Exception {
    if (!Boolean.getBoolean("solr.index.updatelog.enabled")) {
      return;
    }
    doTestFloatPointFieldsAtomicUpdates("number_p_f");
    doTestFloatPointFieldsAtomicUpdates("number_p_f_dv");
    doTestFloatPointFieldsAtomicUpdates("number_p_f_dv_ns");
  }

  @Test
  public void testMultiValuedFloatPointFieldsAtomicUpdates() throws Exception {
    if (!Boolean.getBoolean("solr.index.updatelog.enabled")) {
      return;
    }
    String[] floats = toStringArray(getRandomFloats(3, false));
    doTestMultiValuedPointFieldsAtomicUpdates("number_p_f_mv", "float", floats);
    doTestMultiValuedPointFieldsAtomicUpdates("number_p_f_ni_mv_dv", "float", floats);
    doTestMultiValuedPointFieldsAtomicUpdates("number_p_f_dv_ns_mv", "float", floats);
  }

  @Test
  public void testFloatPointSetQuery() {
    doTestSetQueries("number_p_f", toStringArray(getRandomFloats(20, false)), false);
    doTestSetQueries("number_p_f_dv", toStringArray(getRandomFloats(20, false)), false);
    doTestSetQueries("number_p_f_mv", toStringArray(getRandomFloats(20, false)), true);
    doTestSetQueries("number_p_f_mv_dv", toStringArray(getRandomFloats(20, false)), true);
    doTestSetQueries("number_p_f_ni_dv", toStringArray(getRandomFloats(20, false)), false);
  }

  @Test
  public void testFloatPointFieldNotIndexed() throws Exception {
    String[] floats = toStringArray(getRandomFloats(10, false));
    doTestFieldNotIndexed("number_p_f_ni", floats);
    doTestFieldNotIndexed("number_p_f_ni_mv", floats);
  }

  // Long

  @Test
  public void testLongPointFieldExactQuery() throws Exception {
    doTestIntPointFieldExactQuery("number_p_l", true);
    doTestIntPointFieldExactQuery("number_p_l_mv", true);
    doTestIntPointFieldExactQuery("number_p_l_dv", true);
    doTestIntPointFieldExactQuery("number_p_l_mv_dv", true);
    doTestIntPointFieldExactQuery("number_p_l_ni_dv", true);
    doTestIntPointFieldExactQuery("number_p_l_ni_ns_dv", true);
    doTestIntPointFieldExactQuery("number_p_l_ni_dv_ns", true);
    doTestIntPointFieldExactQuery("number_p_l_ni_mv_dv", true);
  }

  @Test
  public void testLongPointFieldNonSearchableExactQuery() throws Exception {
    doTestIntPointFieldExactQuery("number_p_l_ni", true, false);
    doTestIntPointFieldExactQuery("number_p_l_ni_ns", true, false);
  }

  @Test
  public void testLongPointFieldReturn() throws Exception {
    int numValues = 10 * RANDOM_MULTIPLIER;
    String[] longs = toStringArray(getRandomLongs(numValues, false));
    doTestPointFieldReturn("number_p_l", "long", longs);
    doTestPointFieldReturn("number_p_l_dv_ns", "long", longs);
  }

  @Test
  public void testLongPointFieldRangeQuery() throws Exception {
    doTestIntPointFieldRangeQuery("number_p_l", "long", true);
    doTestIntPointFieldRangeQuery("number_p_l_ni_ns_dv", "long", true);
    doTestIntPointFieldRangeQuery("number_p_l_dv", "long", true);
  }

  @Test
  public void testLongPointFieldNonSearchableRangeQuery() throws Exception {
    doTestPointFieldNonSearchableRangeQuery(
        "number_p_l_ni", toStringArray(getRandomLongs(1, false)));
    doTestPointFieldNonSearchableRangeQuery(
        "number_p_l_ni_ns", toStringArray(getRandomLongs(1, false)));
    int numValues = 2 * RANDOM_MULTIPLIER;
    doTestPointFieldNonSearchableRangeQuery(
        "number_p_l_ni_ns_mv", toStringArray(getRandomLongs(numValues, false)));
  }

  @Test
  public void testLongPointFieldSortAndFunction() throws Exception {
    final SortedSet<String> regexToTest = dynFieldRegexesForType(LongPointField.class);
    final List<Long> vals =
        Arrays.asList(
            (long) Integer.MIN_VALUE,
            1L,
            2L,
            3L,
            4L,
            5L,
            6L,
            7L,
            (long) Integer.MAX_VALUE,
            Long.MAX_VALUE);
    final List<Long> randomLongs = getRandomLongs(10, false);
    final List<Long> randomLongsMissing = getRandomLongs(10, true);

    for (String r :
        Arrays.asList(
            "*_p_l",
            "*_p_l_dv",
            "*_p_l_dv_ns",
            "*_p_l_ni_dv",
            "*_p_l_ni_dv_ns",
            "*_p_l_ni_ns_dv")) {
      assertTrue(r, regexToTest.remove(r));
      String field = r.replace("*", "number");
      doTestPointFieldSort(field, vals);
      doTestPointFieldSort(field, randomLongs);
      doTestLongPointFunctionQuery(field);
    }

    for (String r :
        Arrays.asList(
            "*_p_l_smf",
            "*_p_l_dv_smf",
            "*_p_l_ni_dv_smf",
            "*_p_l_sml",
            "*_p_l_dv_sml",
            "*_p_l_ni_dv_sml")) {
      assertTrue(r, regexToTest.remove(r));
      String field = r.replace("*", "number");
      doTestPointFieldSort(field, vals);
      doTestPointFieldSort(field, randomLongsMissing);
      doTestLongPointFunctionQuery(field);
    }

    // no docvalues
    for (String r : Arrays.asList("*_p_l_ni", "*_p_l_ni_ns")) {
      assertTrue(r, regexToTest.remove(r));
      String field = r.replace("*", "number");
      doTestPointFieldSortError(field, "w/o docValues", toStringArray(getRandomLongs(1, false)));
      doTestPointFieldFunctionQueryError(
          field, "w/o docValues", toStringArray(getRandomLongs(1, false)));
    }

    // multivalued, no docvalues
    for (String r :
        Arrays.asList(
            "*_p_l_mv", "*_p_l_ni_mv", "*_p_l_ni_ns_mv", "*_p_l_mv_smf", "*_p_l_mv_sml")) {

      assertTrue(r, regexToTest.remove(r));
      String field = r.replace("*", "number");
      doTestPointFieldSortError(field, "w/o docValues", toStringArray(getRandomLongs(1, false)));
      int numValues = 2 * RANDOM_MULTIPLIER;
      doTestPointFieldSortError(
          field, "w/o docValues", toStringArray(getRandomLongs(numValues, false)));
      doTestPointFieldFunctionQueryError(
          field, "multivalued", toStringArray(getRandomLongs(1, false)));
      doTestPointFieldFunctionQueryError(
          field, "multivalued", toStringArray(getRandomLongs(numValues, false)));
    }
    // multivalued, w/ docValues
    for (String r :
        Arrays.asList(
            "*_p_l_ni_mv_dv", "*_p_l_ni_dv_ns_mv",
            "*_p_l_dv_ns_mv", "*_p_l_mv_dv",
            "*_p_l_mv_dv_smf", "*_p_l_ni_mv_dv_smf",
            "*_p_l_mv_dv_sml", "*_p_l_ni_mv_dv_sml")) {

      assertTrue(r, regexToTest.remove(r));
      String field = r.replace("*", "number");

      // NOTE: only testing one value per doc here, but TestMinMaxOnMultiValuedField
      // covers this in more depth
      doTestPointFieldSort(field, vals);
      doTestPointFieldSort(field, randomLongs);

      // value source (w/o field(...,min|max)) usage should still error...
      int numValues = 2 * RANDOM_MULTIPLIER;
      doTestPointFieldFunctionQueryError(
          field, "multivalued", toStringArray(getRandomLongs(1, false)));
      doTestPointFieldFunctionQueryError(
          field, "multivalued", toStringArray(getRandomLongs(numValues, false)));
    }
    assertEquals("Missing types in the test", Collections.<String>emptySet(), regexToTest);
  }

  @Test
  public void testLongPointFieldFacetField() throws Exception {
    doTestPointFieldFacetField("number_p_l", "number_p_l_dv", getSequentialStringArrayWithInts(10));
    clearIndex();
    assertU(commit());
    doTestPointFieldFacetField(
        "number_p_l", "number_p_l_dv", toStringArray(getRandomLongs(10, false)));
  }

  @Test
  public void testLongPointFieldRangeFacet() {
    String docValuesField = "number_p_l_dv";
    String nonDocValuesField = "number_p_l";
    int numValues = 10 * RANDOM_MULTIPLIER;
    int numBuckets = numValues / 2;
    List<Long> values;
    List<Long> sortedValues;
    long max;
    do {
      values = getRandomLongs(numValues, false);
      sortedValues = values.stream().sorted().collect(Collectors.toList());
    } while ((max = sortedValues.get(sortedValues.size() - 1))
        >= Long.MAX_VALUE - numValues); // leave room for rounding
    long min = sortedValues.get(0);
    BigInteger bigIntGap =
        BigInteger.valueOf(max + numValues)
            .subtract(BigInteger.valueOf(min))
            .divide(BigInteger.valueOf(numBuckets));
    long gap = bigIntGap.longValueExact();
    int[] bucketCount = new int[numBuckets];
    int bucketNum = 0;
    long minBucketVal = min;
    // System.err.println("min:" + min + "   max: " + max + "   gap: " + gap);
    // System.err.println("bucketNum: " + bucketNum + "   minBucketVal: " + minBucketVal);
    for (Long value : sortedValues) {
      // System.err.println("value: " + value);
      while (BigInteger.valueOf(value)
              .subtract(BigInteger.valueOf(minBucketVal))
              .compareTo(bigIntGap)
          > 0) {
        ++bucketNum;
        minBucketVal += gap;
        // System.err.println("bucketNum: " + bucketNum + "   minBucketVal: " + minBucketVal);
      }
      ++bucketCount[bucketNum];
    }

    for (int i = 0; i < numValues; i++) {
      assertU(
          adoc(
              "id",
              String.valueOf(i),
              docValuesField,
              String.valueOf(values.get(i)),
              nonDocValuesField,
              String.valueOf(values.get(i))));
    }
    assertU(commit());

    assertTrue(h.getCore().getLatestSchema().getField(docValuesField).hasDocValues());
    assertTrue(
        h.getCore().getLatestSchema().getField(docValuesField).getType() instanceof PointField);
    String[] testStrings = new String[numBuckets + 1];
    testStrings[numBuckets] = "//*[@numFound='" + numValues + "']";
    minBucketVal = min;
    for (int i = 0; i < numBuckets; minBucketVal += gap, ++i) {
      testStrings[i] =
          "//lst[@name='facet_counts']/lst[@name='facet_ranges']/lst[@name='"
              + docValuesField
              + "']/lst[@name='counts']/int[@name='"
              + minBucketVal
              + "'][.='"
              + bucketCount[i]
              + "']";
    }
    assertQ(
        req(
            "q",
            "*:*",
            "facet",
            "true",
            "facet.range",
            docValuesField,
            "facet.range.start",
            String.valueOf(min),
            "facet.range.end",
            String.valueOf(max),
            "facet.range.gap",
            String.valueOf(gap)),
        testStrings);
    assertQ(
        req(
            "q",
            "*:*",
            "facet",
            "true",
            "facet.range",
            docValuesField,
            "facet.range.start",
            String.valueOf(min),
            "facet.range.end",
            String.valueOf(max),
            "facet.range.gap",
            String.valueOf(gap),
            "facet.range.method",
            "dv"),
        testStrings);

    assertFalse(h.getCore().getLatestSchema().getField(nonDocValuesField).hasDocValues());
    assertTrue(
        h.getCore().getLatestSchema().getField(nonDocValuesField).getType() instanceof PointField);
    minBucketVal = min;
    for (int i = 0; i < numBuckets; minBucketVal += gap, ++i) {
      testStrings[i] =
          "//lst[@name='facet_counts']/lst[@name='facet_ranges']/lst[@name='"
              + nonDocValuesField
              + "']/lst[@name='counts']/int[@name='"
              + minBucketVal
              + "'][.='"
              + bucketCount[i]
              + "']";
    }
    // Range Faceting with method = filter should work
    assertQ(
        req(
            "q",
            "*:*",
            "facet",
            "true",
            "facet.range",
            nonDocValuesField,
            "facet.range.start",
            String.valueOf(min),
            "facet.range.end",
            String.valueOf(max),
            "facet.range.gap",
            String.valueOf(gap),
            "facet.range.method",
            "filter"),
        testStrings);
    // this should actually use filter method instead of dv
    assertQ(
        req(
            "q",
            "*:*",
            "facet",
            "true",
            "facet.range",
            nonDocValuesField,
            "facet.range.start",
            String.valueOf(min),
            "facet.range.end",
            String.valueOf(max),
            "facet.range.gap",
            String.valueOf(gap),
            "facet.range.method",
            "dv"),
        testStrings);
  }

  @Test
  public void testLongPointStats() {
    int numValues = 10 * RANDOM_MULTIPLIER;
    // don't produce numbers with exponents, since XPath comparison operators can't handle them
    List<Long> values = getRandomLongs(numValues, false, 9999999L);
    List<Long> sortedValues = values.stream().sorted().collect(Collectors.toList());
    double min = (double) sortedValues.get(0);
    double max = (double) sortedValues.get(sortedValues.size() - 1);

    String[] valArray = toStringArray(values);
    doTestPointStats("number_p_l", "number_p_l_dv", valArray, min, max, numValues, 1, 0D);
    doTestPointStats("number_p_l", "number_p_l_mv_dv", valArray, min, max, numValues, 1, 0D);
  }

  @Test
  public void testLongPointFieldMultiValuedExactQuery() throws Exception {
    String[] ints = toStringArray(getRandomInts(20, false));
    doTestPointFieldMultiValuedExactQuery("number_p_l_mv", ints);
    doTestPointFieldMultiValuedExactQuery("number_p_l_ni_mv_dv", ints);
  }

  @Test
  public void testLongPointFieldMultiValuedNonSearchableExactQuery() throws Exception {
    String[] longs = toStringArray(getRandomLongs(20, false));
    doTestPointFieldMultiValuedExactQuery("number_p_l_ni_mv", longs, false);
    doTestPointFieldMultiValuedExactQuery("number_p_l_ni_ns_mv", longs, false);
  }

  @Test
  public void testLongPointFieldMultiValuedReturn() throws Exception {
    String[] longs = toStringArray(getRandomLongs(20, false));
    doTestPointFieldMultiValuedReturn("number_p_l_mv", "long", longs);
    doTestPointFieldMultiValuedReturn("number_p_l_ni_mv_dv", "long", longs);
    doTestPointFieldMultiValuedReturn("number_p_l_dv_ns_mv", "long", longs);
  }

  @Test
  public void testLongPointFieldMultiValuedRangeQuery() throws Exception {
    String[] longs =
        toStringArray(getRandomLongs(20, false).stream().sorted().collect(Collectors.toList()));
    doTestPointFieldMultiValuedRangeQuery("number_p_l_mv", "long", longs);
    doTestPointFieldMultiValuedRangeQuery("number_p_l_ni_mv_dv", "long", longs);
    doTestPointFieldMultiValuedRangeQuery("number_p_l_mv_dv", "long", longs);
  }

  @Test
  public void testLongPointFieldMultiValuedFacetField() throws Exception {
    doTestPointFieldMultiValuedFacetField(
        "number_p_l_mv", "number_p_l_mv_dv", getSequentialStringArrayWithInts(20));
    doTestPointFieldMultiValuedFacetField(
        "number_p_l_mv", "number_p_l_mv_dv", toStringArray(getRandomLongs(20, false)));
  }

  @Test
  public void testLongPointFieldMultiValuedRangeFacet() {
    String docValuesField = "number_p_l_mv_dv";
    String nonDocValuesField = "number_p_l_mv";
    int numValues = 20 * RANDOM_MULTIPLIER;
    int numBuckets = numValues / 2;
    List<Long> values;
    List<PosVal<Long>> sortedValues;
    long max;
    do {
      values = getRandomLongs(numValues, false);
      sortedValues = toAscendingPosVals(values, true);
    } while ((max = sortedValues.get(sortedValues.size() - 1).val)
        >= Long.MAX_VALUE - numValues); // leave room for rounding
    long min = sortedValues.get(0).val;
    long gap =
        BigInteger.valueOf(max + numValues)
            .subtract(BigInteger.valueOf(min))
            .divide(BigInteger.valueOf(numBuckets))
            .longValueExact();
    List<Set<Integer>> docIdBucket = new ArrayList<>(numBuckets);
    for (int i = 0; i < numBuckets; ++i) {
      docIdBucket.add(new HashSet<>());
    }
    int bucketNum = 0;
    long minBucketVal = min;
    for (PosVal<Long> value : sortedValues) {
      while (value.val - minBucketVal >= gap) {
        ++bucketNum;
        minBucketVal += gap;
      }
      docIdBucket.get(bucketNum).add(value.pos / 2); // each doc gets two consecutive values
    }
    for (int i = 0; i < numValues; i += 2) {
      assertU(
          adoc(
              "id",
              String.valueOf(i / 2),
              docValuesField,
              String.valueOf(values.get(i)),
              docValuesField,
              String.valueOf(values.get(i + 1)),
              nonDocValuesField,
              String.valueOf(values.get(i)),
              nonDocValuesField,
              String.valueOf(values.get(i + 1))));
    }
    assertU(commit());
    assertTrue(h.getCore().getLatestSchema().getField(docValuesField).hasDocValues());
    assertTrue(
        h.getCore().getLatestSchema().getField(docValuesField).getType() instanceof PointField);
    String[] testStrings = new String[numBuckets + 1];
    testStrings[numBuckets] = "//*[@numFound='" + (numValues / 2) + "']";
    minBucketVal = min;
    for (int i = 0; i < numBuckets; minBucketVal += gap, ++i) {
      testStrings[i] =
          "//lst[@name='facet_counts']/lst[@name='facet_ranges']/lst[@name='"
              + docValuesField
              + "']/lst[@name='counts']/int[@name='"
              + minBucketVal
              + "'][.='"
              + docIdBucket.get(i).size()
              + "']";
    }

    assertQ(
        req(
            "q",
            "*:*",
            "facet",
            "true",
            "facet.range",
            docValuesField,
            "facet.range.start",
            String.valueOf(min),
            "facet.range.end",
            String.valueOf(max),
            "facet.range.gap",
            String.valueOf(gap),
            "indent",
            "on"),
        testStrings);
    assertQ(
        req(
            "q",
            "*:*",
            "facet",
            "true",
            "facet.range",
            docValuesField,
            "facet.range.start",
            String.valueOf(min),
            "facet.range.end",
            String.valueOf(max),
            "facet.range.gap",
            String.valueOf(gap),
            "facet.range.method",
            "dv",
            "indent",
            "on"),
        testStrings);

    assertFalse(h.getCore().getLatestSchema().getField(nonDocValuesField).hasDocValues());
    assertTrue(
        h.getCore().getLatestSchema().getField(nonDocValuesField).getType() instanceof PointField);
    minBucketVal = min;
    for (int i = 0; i < numBuckets; minBucketVal += gap, ++i) {
      testStrings[i] =
          "//lst[@name='facet_counts']/lst[@name='facet_ranges']/lst[@name='"
              + nonDocValuesField
              + "']/lst[@name='counts']/int[@name='"
              + minBucketVal
              + "'][.='"
              + docIdBucket.get(i).size()
              + "']";
    }
    // Range Faceting with method = filter should work
    assertQ(
        req(
            "q",
            "*:*",
            "facet",
            "true",
            "facet.range",
            nonDocValuesField,
            "facet.range.start",
            String.valueOf(min),
            "facet.range.end",
            String.valueOf(max),
            "facet.range.gap",
            String.valueOf(gap),
            "facet.range.method",
            "filter",
            "indent",
            "on"),
        testStrings);
    // this should actually use filter method instead of dv
    assertQ(
        req(
            "q",
            "*:*",
            "facet",
            "true",
            "facet.range",
            nonDocValuesField,
            "facet.range.start",
            String.valueOf(min),
            "facet.range.end",
            String.valueOf(max),
            "facet.range.gap",
            String.valueOf(gap),
            "facet.range.method",
            "dv",
            "indent",
            "on"),
        testStrings);
  }

  @Test
  public void testLongPointMultiValuedFunctionQuery() throws Exception {
    doTestPointMultiValuedFunctionQuery(
        "number_p_l_mv", "number_p_l_mv_dv", getSequentialStringArrayWithInts(20));
    doTestPointMultiValuedFunctionQuery(
        "number_p_l_mv",
        "number_p_l_mv_dv",
        toStringArray(getRandomLongs(20, false).stream().sorted().collect(Collectors.toList())));
  }

  @Test
  public void testLongPointFieldsAtomicUpdates() throws Exception {
    if (!Boolean.getBoolean("solr.index.updatelog.enabled")) {
      return;
    }
    doTestLongPointFieldsAtomicUpdates("number_p_l");
    doTestLongPointFieldsAtomicUpdates("number_p_l_dv");
    doTestLongPointFieldsAtomicUpdates("number_p_l_dv_ns");
  }

  @Test
  public void testMultiValuedLongPointFieldsAtomicUpdates() throws Exception {
    if (!Boolean.getBoolean("solr.index.updatelog.enabled")) {
      return;
    }
    String[] longs = toStringArray(getRandomLongs(3, false));
    doTestMultiValuedPointFieldsAtomicUpdates("number_p_l_mv", "long", longs);
    doTestMultiValuedPointFieldsAtomicUpdates("number_p_l_ni_mv_dv", "long", longs);
    doTestMultiValuedPointFieldsAtomicUpdates("number_p_l_dv_ns_mv", "long", longs);
  }

  @Test
  public void testLongPointSetQuery() {
    doTestSetQueries("number_p_l", toStringArray(getRandomLongs(20, false)), false);
    doTestSetQueries("number_p_l_dv", toStringArray(getRandomLongs(20, false)), false);
    doTestSetQueries("number_p_l_mv", toStringArray(getRandomLongs(20, false)), true);
    doTestSetQueries("number_p_l_mv_dv", toStringArray(getRandomLongs(20, false)), true);
    doTestSetQueries("number_p_l_ni_dv", toStringArray(getRandomLongs(20, false)), false);
  }

  @Test
  public void testLongPointFieldNotIndexed() throws Exception {
    String[] longs = toStringArray(getRandomLongs(10, false));
    doTestFieldNotIndexed("number_p_l_ni", longs);
    doTestFieldNotIndexed("number_p_l_ni_mv", longs);
  }

  // Date

  private String getRandomDateMaybeWithMath() {
    long millis1 = random().nextLong() % MAX_DATE_EPOCH_MILLIS;
    String date = Instant.ofEpochMilli(millis1).toString();
    if (random().nextBoolean()) {
      long millis2 = random().nextLong() % MAX_DATE_EPOCH_MILLIS;
      DateGapCeiling gap = new DateGapCeiling(millis2 - millis1);
      date += gap.toString();
    }
    return date;
  }

  @Test
  public void testDatePointFieldExactQuery() throws Exception {
    String baseDate = getRandomDateMaybeWithMath();
    for (String field :
        Arrays.asList(
            "number_p_dt",
            "number_p_dt_mv",
            "number_p_dt_dv",
            "number_p_dt_mv_dv",
            "number_p_dt_ni_dv",
            "number_p_dt_ni_ns_dv",
            "number_p_dt_ni_mv_dv")) {
      doTestDatePointFieldExactQuery(field, baseDate);
    }
  }

  @Test
  public void testDatePointFieldNonSearchableExactQuery() throws Exception {
    doTestDatePointFieldExactQuery("number_p_dt_ni", "1995-12-31T23:59:59Z", false);
    doTestDatePointFieldExactQuery("number_p_dt_ni_ns", "1995-12-31T23:59:59Z", false);
  }

  @Test
  public void testDatePointFieldReturn() throws Exception {
    int numValues = 10 * RANDOM_MULTIPLIER;
    String[] dates = toStringArray(getRandomInstants(numValues, false));
    doTestPointFieldReturn("number_p_dt", "date", dates);
    doTestPointFieldReturn("number_p_dt_dv_ns", "date", dates);
  }

  @Test
  public void testDatePointFieldRangeQuery() throws Exception {
    doTestDatePointFieldRangeQuery("number_p_dt");
    doTestDatePointFieldRangeQuery("number_p_dt_ni_ns_dv");
  }

  @Test
  public void testDatePointFieldNonSearchableRangeQuery() throws Exception {
    doTestPointFieldNonSearchableRangeQuery(
        "number_p_dt_ni", toStringArray(getRandomInstants(1, false)));
    doTestPointFieldNonSearchableRangeQuery(
        "number_p_dt_ni_ns", toStringArray(getRandomInstants(1, false)));
    int numValues = 2 * RANDOM_MULTIPLIER;
    doTestPointFieldNonSearchableRangeQuery(
        "number_p_dt_ni_ns_mv", toStringArray(getRandomInstants(numValues, false)));
  }

  @Test
  public void testDatePointFieldSortAndFunction() throws Exception {
    final SortedSet<String> regexToTest = dynFieldRegexesForType(DatePointField.class);
    final List<String> sequential = Arrays.asList(getSequentialStringArrayWithDates(10));
    final List<Instant> randomDates = getRandomInstants(10, false);
    final List<Instant> randomDatesMissing = getRandomInstants(10, true);

    for (String r :
        Arrays.asList(
            "*_p_dt",
            "*_p_dt_dv",
            "*_p_dt_dv_ns",
            "*_p_dt_ni_dv",
            "*_p_dt_ni_dv_ns",
            "*_p_dt_ni_ns_dv")) {
      assertTrue(r, regexToTest.remove(r));
      String field = r.replace("*", "number");
      doTestPointFieldSort(field, sequential);
      doTestPointFieldSort(field, randomDates);
      doTestDatePointFunctionQuery(field);
    }
    for (String r :
        Arrays.asList(
            "*_p_dt_smf",
            "*_p_dt_dv_smf",
            "*_p_dt_ni_dv_smf",
            "*_p_dt_sml",
            "*_p_dt_dv_sml",
            "*_p_dt_ni_dv_sml")) {
      assertTrue(r, regexToTest.remove(r));
      String field = r.replace("*", "number");
      doTestPointFieldSort(field, sequential);
      doTestPointFieldSort(field, randomDatesMissing);
      doTestDatePointFunctionQuery(field);
    }

    for (String r : Arrays.asList("*_p_dt_ni", "*_p_dt_ni_ns")) {
      assertTrue(r, regexToTest.remove(r));
      String field = r.replace("*", "number");
      doTestPointFieldSortError(field, "w/o docValues", "1995-12-31T23:59:59Z");
      doTestPointFieldFunctionQueryError(field, "w/o docValues", "1995-12-31T23:59:59Z");
    }

    // multivalued, no docvalues
    for (String r :
        Arrays.asList(
            "*_p_dt_mv", "*_p_dt_ni_mv", "*_p_dt_ni_ns_mv", "*_p_dt_mv_smf", "*_p_dt_mv_sml")) {

      assertTrue(r, regexToTest.remove(r));
      String field = r.replace("*", "number");
      doTestPointFieldSortError(field, "w/o docValues", "1995-12-31T23:59:59Z");
      doTestPointFieldSortError(
          field, "w/o docValues", "1995-12-31T23:59:59Z", "2000-12-31T23:59:59Z");
      doTestPointFieldFunctionQueryError(field, "multivalued", "1995-12-31T23:59:59Z");
      doTestPointFieldFunctionQueryError(
          field, "multivalued", "1995-12-31T23:59:59Z", "2000-12-31T23:59:59Z");
    }

    // multivalued, w/ docValues
    for (String r :
        Arrays.asList(
            "*_p_dt_ni_mv_dv", "*_p_dt_ni_dv_ns_mv",
            "*_p_dt_dv_ns_mv", "*_p_dt_mv_dv",
            "*_p_dt_mv_dv_smf", "*_p_dt_ni_mv_dv_smf",
            "*_p_dt_mv_dv_sml", "*_p_dt_ni_mv_dv_sml")) {
      assertTrue(r, regexToTest.remove(r));
      String field = r.replace("*", "number");

      // NOTE: only testing one value per doc here, but TestMinMaxOnMultiValuedField
      // covers this in more depth
      doTestPointFieldSort(field, sequential);
      doTestPointFieldSort(field, randomDates);

      // value source (w/o field(...,min|max)) usage should still error...
      doTestPointFieldFunctionQueryError(field, "multivalued", "1995-12-31T23:59:59Z");
      doTestPointFieldFunctionQueryError(
          field, "multivalued", "1995-12-31T23:59:59Z", "2000-12-31T23:59:59Z");
    }
    assertEquals("Missing types in the test", Collections.<String>emptySet(), regexToTest);
  }

  @Test
  public void testDatePointFieldFacetField() throws Exception {
    doTestPointFieldFacetField(
        "number_p_dt", "number_p_dt_dv", getSequentialStringArrayWithDates(10));
    clearIndex();
    assertU(commit());
    doTestPointFieldFacetField(
        "number_p_dt", "number_p_dt_dv", toStringArray(getRandomInstants(10, false)));
  }

  private static class DateGapCeiling {
    String calendarUnit = "MILLIS";
    long inCalendarUnits;
    boolean negative = false;

    /** Maximize calendar unit size given initialGapMillis; performs ceiling on each conversion */
    DateGapCeiling(long initialGapMillis) {
      negative = initialGapMillis < 0;
      inCalendarUnits = Math.abs(initialGapMillis);
      if (inCalendarUnits >= 1000L) {
        calendarUnit = "SECS";
        inCalendarUnits = (inCalendarUnits + 999L) / 1000L;
        if (inCalendarUnits >= 60L) {
          calendarUnit = "MINUTES";
          inCalendarUnits = (inCalendarUnits + 59L) / 60L;
          if (inCalendarUnits >= 60L) {
            calendarUnit = "HOURS";
            inCalendarUnits = (inCalendarUnits + 59L) / 60L;
            if (inCalendarUnits >= 24L) {
              calendarUnit = "DAYS";
              inCalendarUnits = (inCalendarUnits + 23L) / 24L;
              if (inCalendarUnits >= 12L) {
                calendarUnit = "MONTHS";
                inCalendarUnits = (inCalendarUnits + 11L) / 12L;
                // 487 = 365.25 / 12 * 16   (365.25 days/year, -ish)
                if ((inCalendarUnits * 16) >= 487) {
                  calendarUnit = "YEARS";
                  inCalendarUnits = (16L * inCalendarUnits + 486) / 487L;
                }
              }
            }
          }
        }
      }
    }

    @Override
    public String toString() {
      return (negative ? "-" : "+") + inCalendarUnits + calendarUnit;
    }

    public long addTo(long millis) {
      // Instant.plus() doesn't work with estimated durations (MONTHS and YEARS)
      LocalDateTime time =
          LocalDateTime.ofInstant(Instant.ofEpochMilli(millis), ZoneOffset.ofHours(0));
      if (negative) {
        time = time.minus(inCalendarUnits, DateMathParser.CALENDAR_UNITS.get(calendarUnit));
      } else {
        time = time.plus(inCalendarUnits, DateMathParser.CALENDAR_UNITS.get(calendarUnit));
      }
      return time.atZone(ZoneOffset.ofHours(0)).toInstant().toEpochMilli();
    }
  }

  @Test
  public void testDatePointFieldRangeFacet() {
    String docValuesField = "number_p_dt_dv";
    String nonDocValuesField = "number_p_dt";
    int numValues = 10 * RANDOM_MULTIPLIER;
    int numBuckets = numValues / 2;
    List<Long> values, sortedValues;
    long min, max;
    DateGapCeiling gap;
    do {
      values = getRandomLongs(numValues, false, MAX_DATE_EPOCH_MILLIS);
      sortedValues = values.stream().sorted().collect(Collectors.toList());
      min = sortedValues.get(0);
      max = sortedValues.get(sortedValues.size() - 1);
    } while (max > MAX_DATE_EPOCH_MILLIS || min < MIN_DATE_EPOCH_MILLIS);
    long initialGap =
        BigInteger.valueOf(max)
            .subtract(BigInteger.valueOf(min))
            .divide(BigInteger.valueOf(numBuckets))
            .longValueExact();
    gap =
        new DateGapCeiling(
            BigInteger.valueOf(max + initialGap)
                .subtract(BigInteger.valueOf(min)) // padding for rounding
                .divide(BigInteger.valueOf(numBuckets))
                .longValueExact());
    int[] bucketCount = new int[numBuckets];
    int bucketNum = 0;
    long minBucketVal = min;
    // System.err.println("min:" + Instant.ofEpochMilli(min) + "   max: " +
    // Instant.ofEpochMilli(max) + "   gap: " + gap);
    // System.err.println("bucketNum: " + bucketNum + "   minBucketVal: " +
    // Instant.ofEpochMilli(minBucketVal));
    for (long value : sortedValues) {
      // System.err.println("value: " + Instant.ofEpochMilli(value));
      while (value >= gap.addTo(minBucketVal)) {
        ++bucketNum;
        minBucketVal = gap.addTo(minBucketVal);
        // System.err.println("bucketNum: " + bucketNum + "   minBucketVal: " +
        // Instant.ofEpochMilli(minBucketVal));
      }
      ++bucketCount[bucketNum];
    }

    for (int i = 0; i < numValues; i++) {
      assertU(
          adoc(
              "id",
              String.valueOf(i),
              docValuesField,
              Instant.ofEpochMilli(values.get(i)).toString(),
              nonDocValuesField,
              Instant.ofEpochMilli(values.get(i)).toString()));
    }
    assertU(commit());

    assertTrue(h.getCore().getLatestSchema().getField(docValuesField).hasDocValues());
    assertTrue(
        h.getCore().getLatestSchema().getField(docValuesField).getType() instanceof PointField);
    String[] testStrings = new String[numBuckets + 1];
    testStrings[numBuckets] = "//*[@numFound='" + numValues + "']";
    minBucketVal = min;
    for (int i = 0; i < numBuckets; ++i) {
      testStrings[i] =
          "//lst[@name='facet_counts']/lst[@name='facet_ranges']/lst[@name='"
              + docValuesField
              + "']/lst[@name='counts']/int[@name='"
              + Instant.ofEpochMilli(minBucketVal)
              + "'][.='"
              + bucketCount[i]
              + "']";
      minBucketVal = gap.addTo(minBucketVal);
    }
    long maxPlusGap = gap.addTo(max);
    assertQ(
        req(
            "q",
            "*:*",
            "facet",
            "true",
            "facet.range",
            docValuesField,
            "facet.range.start",
            Instant.ofEpochMilli(min).toString(),
            "facet.range.end",
            Instant.ofEpochMilli(maxPlusGap).toString(),
            "facet.range.gap",
            gap.toString()),
        testStrings);
    assertQ(
        req(
            "q",
            "*:*",
            "facet",
            "true",
            "facet.range",
            docValuesField,
            "facet.range.start",
            Instant.ofEpochMilli(min).toString(),
            "facet.range.end",
            Instant.ofEpochMilli(maxPlusGap).toString(),
            "facet.range.gap",
            gap.toString(),
            "facet.range.method",
            "dv"),
        testStrings);

    assertFalse(h.getCore().getLatestSchema().getField(nonDocValuesField).hasDocValues());
    assertTrue(
        h.getCore().getLatestSchema().getField(nonDocValuesField).getType() instanceof PointField);
    minBucketVal = min;
    for (int i = 0; i < numBuckets; ++i) {
      testStrings[i] =
          "//lst[@name='facet_counts']/lst[@name='facet_ranges']/lst[@name='"
              + nonDocValuesField
              + "']/lst[@name='counts']/int[@name='"
              + Instant.ofEpochMilli(minBucketVal).toString()
              + "'][.='"
              + bucketCount[i]
              + "']";
      minBucketVal = gap.addTo(minBucketVal);
    }
    maxPlusGap = gap.addTo(max);
    // Range Faceting with method = filter should work
    assertQ(
        req(
            "q",
            "*:*",
            "facet",
            "true",
            "facet.range",
            nonDocValuesField,
            "facet.range.start",
            Instant.ofEpochMilli(min).toString(),
            "facet.range.end",
            Instant.ofEpochMilli(maxPlusGap).toString(),
            "facet.range.gap",
            gap.toString(),
            "facet.range.method",
            "filter"),
        testStrings);
    // this should actually use filter method instead of dv
    assertQ(
        req(
            "q",
            "*:*",
            "facet",
            "true",
            "facet.range",
            nonDocValuesField,
            "facet.range.start",
            Instant.ofEpochMilli(min).toString(),
            "facet.range.end",
            Instant.ofEpochMilli(maxPlusGap).toString(),
            "facet.range.gap",
            gap.toString(),
            "facet.range.method",
            "dv"),
        testStrings);
  }

  @Test
  public void testDatePointStats() {
    String[] randomSortedDates = toAscendingStringArray(getRandomInstants(10, false), true);
    doTestDatePointStats("number_p_dt", "number_p_dt_dv", randomSortedDates);
    doTestDatePointStats("number_p_dt_mv", "number_p_dt_mv_dv", randomSortedDates);
  }

  @Test
  public void testDatePointFieldMultiValuedExactQuery() throws Exception {
    String[] dates = toStringArray(getRandomInstants(20, false));
    doTestPointFieldMultiValuedExactQuery("number_p_dt_mv", dates);
    doTestPointFieldMultiValuedExactQuery("number_p_dt_ni_mv_dv", dates);
  }

  @Test
  public void testDatePointFieldMultiValuedNonSearchableExactQuery() throws Exception {
    String[] dates = toStringArray(getRandomInstants(20, false));
    doTestPointFieldMultiValuedExactQuery("number_p_dt_ni_mv", dates, false);
    doTestPointFieldMultiValuedExactQuery("number_p_dt_ni_ns_mv", dates, false);
  }

  @Test
  public void testDatePointFieldMultiValuedReturn() throws Exception {
    String[] dates = toStringArray(getRandomInstants(20, false));
    doTestPointFieldMultiValuedReturn("number_p_dt_mv", "date", dates);
    doTestPointFieldMultiValuedReturn("number_p_dt_ni_mv_dv", "date", dates);
    doTestPointFieldMultiValuedReturn("number_p_dt_dv_ns_mv", "date", dates);
  }

  @Test
  public void testDatePointFieldMultiValuedRangeQuery() throws Exception {
    String[] dates =
        toStringArray(getRandomInstants(20, false).stream().sorted().collect(Collectors.toList()));
    doTestPointFieldMultiValuedRangeQuery("number_p_dt_mv", "date", dates);
    doTestPointFieldMultiValuedRangeQuery("number_p_dt_ni_mv_dv", "date", dates);
  }

  @Test
  public void testDatePointFieldMultiValuedFacetField() throws Exception {
    doTestPointFieldMultiValuedFacetField(
        "number_p_dt_mv", "number_p_dt_mv_dv", getSequentialStringArrayWithDates(20));
    doTestPointFieldMultiValuedFacetField(
        "number_p_dt_mv", "number_p_dt_mv_dv", toStringArray(getRandomInstants(20, false)));
  }

  @Test
  public void testDatePointFieldMultiValuedRangeFacet() {
    String docValuesField = "number_p_dt_mv_dv";
    SchemaField dvSchemaField = h.getCore().getLatestSchema().getField(docValuesField);
    assertTrue(dvSchemaField.multiValued());
    assertTrue(dvSchemaField.hasDocValues());
    assertTrue(dvSchemaField.getType() instanceof PointField);

    String nonDocValuesField = "number_p_dt_mv";
    SchemaField nonDvSchemaField = h.getCore().getLatestSchema().getField(nonDocValuesField);
    assertTrue(nonDvSchemaField.multiValued());
    assertFalse(nonDvSchemaField.hasDocValues());
    assertTrue(nonDvSchemaField.getType() instanceof PointField);

    int numValues = 20 * RANDOM_MULTIPLIER;
    int numBuckets = numValues / 2;
    List<Long> values;
    List<PosVal<Long>> sortedValues;
    long min, max;
    do {
      values = getRandomLongs(numValues, false, MAX_DATE_EPOCH_MILLIS);
      sortedValues = toAscendingPosVals(values, true);
      min = sortedValues.get(0).val;
      max = sortedValues.get(sortedValues.size() - 1).val;
    } while (max > MAX_DATE_EPOCH_MILLIS || min < MIN_DATE_EPOCH_MILLIS);
    long initialGap =
        BigInteger.valueOf(max)
            .subtract(BigInteger.valueOf(min))
            .divide(BigInteger.valueOf(numBuckets))
            .longValueExact();
    DateGapCeiling gap =
        new DateGapCeiling(
            BigInteger.valueOf(max + initialGap)
                .subtract(BigInteger.valueOf(min)) // padding for rounding
                .divide(BigInteger.valueOf(numBuckets))
                .longValueExact());
    List<Set<Integer>> docIdBucket = new ArrayList<>(numBuckets);
    for (int i = 0; i < numBuckets; ++i) {
      docIdBucket.add(new HashSet<>());
    }
    int bucketNum = 0;
    long minBucketVal = min;
    // System.err.println("min:" + Instant.ofEpochMilli(min) + "   max: " +
    // Instant.ofEpochMilli(max) + "   gap: " + gap);
    // System.err.println("bucketNum: " + bucketNum + "   minBucketVal: " +
    // Instant.ofEpochMilli(minBucketVal));
    for (PosVal<Long> value : sortedValues) {
      // System.err.println("value: " + Instant.ofEpochMilli(value.val));
      while (value.val >= gap.addTo(minBucketVal)) {
        ++bucketNum;
        minBucketVal = gap.addTo(minBucketVal);
        // System.err.println("bucketNum: " + bucketNum + "   minBucketVal: " +
        // Instant.ofEpochMilli(minBucketVal));
      }
      docIdBucket.get(bucketNum).add(value.pos / 2); // each doc gets two consecutive values
    }
    for (int i = 0; i < numValues; i += 2) {
      assertU(
          adoc(
              "id",
              String.valueOf(i / 2),
              docValuesField,
              Instant.ofEpochMilli(values.get(i)).toString(),
              docValuesField,
              Instant.ofEpochMilli(values.get(i + 1)).toString(),
              nonDocValuesField,
              Instant.ofEpochMilli(values.get(i)).toString(),
              nonDocValuesField,
              Instant.ofEpochMilli(values.get(i + 1)).toString()));
    }
    assertU(commit());

    String minDate = Instant.ofEpochMilli(min).toString();
    String maxDate = Instant.ofEpochMilli(max).toString();
    String[] testStrings = new String[numBuckets + 1];
    testStrings[numBuckets] = "//*[@numFound='" + (numValues / 2) + "']";
    minBucketVal = min;
    for (int i = 0; i < numBuckets; minBucketVal = gap.addTo(minBucketVal), ++i) {
      testStrings[i] =
          "//lst[@name='facet_counts']/lst[@name='facet_ranges']/lst[@name='"
              + docValuesField
              + "']/lst[@name='counts']/int[@name='"
              + Instant.ofEpochMilli(minBucketVal)
              + "'][.='"
              + docIdBucket.get(i).size()
              + "']";
    }

    assertQ(
        req(
            "q",
            "*:*",
            "facet",
            "true",
            "facet.range",
            docValuesField,
            "facet.range.start",
            minDate,
            "facet.range.end",
            maxDate,
            "facet.range.gap",
            gap.toString(),
            "indent",
            "on"),
        testStrings);
    assertQ(
        req(
            "q",
            "*:*",
            "facet",
            "true",
            "facet.range",
            docValuesField,
            "facet.range.start",
            minDate,
            "facet.range.end",
            maxDate,
            "facet.range.gap",
            gap.toString(),
            "facet.range.method",
            "dv",
            "indent",
            "on"),
        testStrings);

    minBucketVal = min;
    for (int i = 0; i < numBuckets; minBucketVal = gap.addTo(minBucketVal), ++i) {
      testStrings[i] =
          "//lst[@name='facet_counts']/lst[@name='facet_ranges']/lst[@name='"
              + nonDocValuesField
              + "']/lst[@name='counts']/int[@name='"
              + Instant.ofEpochMilli(minBucketVal)
              + "'][.='"
              + docIdBucket.get(i).size()
              + "']";
    }
    // Range Faceting with method = filter should work
    assertQ(
        req(
            "q",
            "*:*",
            "facet",
            "true",
            "facet.range",
            nonDocValuesField,
            "facet.range.start",
            minDate,
            "facet.range.end",
            maxDate,
            "facet.range.gap",
            gap.toString(),
            "facet.range.method",
            "filter",
            "indent",
            "on"),
        testStrings);
    // this should actually use filter method instead of dv
    assertQ(
        req(
            "q",
            "*:*",
            "facet",
            "true",
            "facet.range",
            nonDocValuesField,
            "facet.range.start",
            minDate,
            "facet.range.end",
            maxDate,
            "facet.range.gap",
            gap.toString(),
            "facet.range.method",
            "dv",
            "indent",
            "on"),
        testStrings);
  }

  @Test
  public void testDatePointMultiValuedFunctionQuery() throws Exception {
    String[] dates =
        toStringArray(getRandomInstants(20, false).stream().sorted().collect(Collectors.toList()));
    doTestPointMultiValuedFunctionQuery("number_p_dt_mv", "number_p_dt_mv_dv", dates);
  }

  @Test
  public void testDatePointFieldsAtomicUpdates() throws Exception {
    if (!Boolean.getBoolean("solr.index.updatelog.enabled")) {
      return;
    }
    doTestDatePointFieldsAtomicUpdates("number_p_dt");
    doTestDatePointFieldsAtomicUpdates("number_p_dt_dv");
    doTestDatePointFieldsAtomicUpdates("number_p_dt_dv_ns");
  }

  @Test
  public void testMultiValuedDatePointFieldsAtomicUpdates() throws Exception {
    if (!Boolean.getBoolean("solr.index.updatelog.enabled")) {
      return;
    }
    String[] dates =
        getRandomLongs(3, false, MAX_DATE_EPOCH_MILLIS).stream()
            .map(Instant::ofEpochMilli)
            .map(Object::toString)
            .toArray(String[]::new);
    doTestMultiValuedPointFieldsAtomicUpdates("number_p_dt_mv", "date", dates);
    doTestMultiValuedPointFieldsAtomicUpdates("number_p_dt_ni_mv_dv", "date", dates);
    doTestMultiValuedPointFieldsAtomicUpdates("number_p_dt_dv_ns_mv", "date", dates);
  }

  @Test
  public void testDatePointSetQuery() {
    doTestSetQueries("number_p_dt", toStringArray(getRandomInstants(20, false)), false);
    doTestSetQueries("number_p_dt_dv", toStringArray(getRandomInstants(20, false)), false);
    doTestSetQueries("number_p_dt_mv", toStringArray(getRandomInstants(20, false)), true);
    doTestSetQueries("number_p_dt_mv_dv", toStringArray(getRandomInstants(20, false)), true);
    doTestSetQueries("number_p_dt_ni_dv", toStringArray(getRandomInstants(20, false)), false);
  }

  @Test
  public void testDatePointFieldNotIndexed() throws Exception {
    String[] dates = toStringArray(getRandomInstants(10, false));
    doTestFieldNotIndexed("number_p_dt_ni", dates);
    doTestFieldNotIndexed("number_p_dt_ni_mv", dates);
  }

  @Test
  public void testIndexOrDocValuesQuery() {
    String[] fieldTypeNames = new String[] {"_p_i", "_p_l", "_p_d", "_p_f", "_p_dt"};
    FieldType[] fieldTypes =
        new FieldType[] {
          new IntPointField(),
          new LongPointField(),
          new DoublePointField(),
          new FloatPointField(),
          new DatePointField()
        };
    String[] ints =
        toStringArray(getRandomInts(2, false).stream().sorted().collect(Collectors.toList()));
    String[] longs =
        toStringArray(getRandomLongs(2, false).stream().sorted().collect(Collectors.toList()));
    String[] doubles =
        toStringArray(getRandomDoubles(2, false).stream().sorted().collect(Collectors.toList()));
    String[] floats =
        toStringArray(getRandomFloats(2, false).stream().sorted().collect(Collectors.toList()));
    String[] dates =
        toStringArray(getRandomInstants(2, false).stream().sorted().collect(Collectors.toList()));
    String[] min = new String[] {ints[0], longs[0], doubles[0], floats[0], dates[0]};
    String[] max = new String[] {ints[1], longs[1], doubles[1], floats[1], dates[1]};
    assertTrue(
        fieldTypeNames.length == fieldTypes.length
            && fieldTypeNames.length == max.length
            && fieldTypeNames.length == min.length);
    for (int i = 0; i < fieldTypeNames.length; i++) {
      SchemaField fieldIndexed = h.getCore().getLatestSchema().getField("foo_" + fieldTypeNames[i]);
      SchemaField fieldIndexedAndDv =
          h.getCore().getLatestSchema().getField("foo_" + fieldTypeNames[i] + "_dv");
      SchemaField fieldIndexedMv =
          h.getCore().getLatestSchema().getField("foo_" + fieldTypeNames[i] + "_mv");
      SchemaField fieldIndexedAndDvMv =
          h.getCore().getLatestSchema().getField("foo_" + fieldTypeNames[i] + "_mv_dv");
      assertTrue(
          fieldTypes[i].getRangeQuery(null, fieldIndexed, min[i], max[i], true, true)
              instanceof PointRangeQuery);
      assertTrue(
          fieldTypes[i].getRangeQuery(null, fieldIndexedAndDv, min[i], max[i], true, true)
              instanceof IndexOrDocValuesQuery);
      assertTrue(
          fieldTypes[i].getRangeQuery(null, fieldIndexedMv, min[i], max[i], true, true)
              instanceof PointRangeQuery);
      assertTrue(
          fieldTypes[i].getRangeQuery(null, fieldIndexedAndDvMv, min[i], max[i], true, true)
              instanceof IndexOrDocValuesQuery);
      assertTrue(
          fieldTypes[i].getFieldQuery(null, fieldIndexed, min[i]) instanceof PointRangeQuery);
      assertTrue(
          fieldTypes[i].getFieldQuery(null, fieldIndexedAndDv, min[i])
              instanceof IndexOrDocValuesQuery);
      assertTrue(
          fieldTypes[i].getFieldQuery(null, fieldIndexedMv, min[i]) instanceof PointRangeQuery);
      assertTrue(
          fieldTypes[i].getFieldQuery(null, fieldIndexedAndDvMv, min[i])
              instanceof IndexOrDocValuesQuery);
    }
  }

  public void testInternals() throws IOException {
    String[] types = new String[] {"i", "l", "f", "d", "dt"};
    String[][] values =
        new String[][] {
          toStringArray(getRandomInts(10, false)),
          toStringArray(getRandomLongs(10, false)),
          toStringArray(getRandomFloats(10, false)),
          toStringArray(getRandomDoubles(10, false)),
          toStringArray(getRandomInstants(10, false))
        };
    assertEquals(types.length, values.length);
    Set<String> typesTested = new HashSet<>();
    for (int i = 0; i < types.length; ++i) {
      for (String suffix : FIELD_SUFFIXES) {
        doTestInternals("number_p_" + types[i] + suffix, values[i]);
        typesTested.add("*_p_" + types[i] + suffix);
      }
    }
    assertEquals(
        "Missing types in the test", dynFieldRegexesForType(PointField.class), typesTested);
  }

  // Helper methods

  /**
   * Given a FieldType, return the list of DynamicField 'regexes' for all declared DynamicFields
   * that use that FieldType.
   *
   * @see IndexSchema#getDynamicFields
   * @see DynamicField#getRegex
   */
  private static SortedSet<String> dynFieldRegexesForType(final Class<? extends FieldType> clazz) {
    SortedSet<String> typesToTest = new TreeSet<>();
    for (DynamicField dynField : h.getCore().getLatestSchema().getDynamicFields()) {
      if (clazz.isInstance(dynField.getPrototype().getType())) {
        typesToTest.add(dynField.getRegex());
      }
    }
    return typesToTest;
  }

  private <T> List<T> getRandomList(int length, boolean missingVals, Supplier<T> randomVal) {
    List<T> list = new ArrayList<>(length);
    for (int i = 0; i < length; ++i) {
      T val = null;
      // Sometimes leave val as null when we're producing missing values
      if (missingVals == false || usually()) {
        val = randomVal.get();
      }
      list.add(val);
    }
    return list;
  }

  private List<Double> getRandomDoubles(int length, boolean missingVals) {
    return getRandomList(
        length,
        missingVals,
        () -> {
          Double d = Double.NaN;
          while (d.isNaN()) {
            d = Double.longBitsToDouble(random().nextLong());
          }
          return d;
        });
  }

  private List<Float> getRandomFloats(int length, boolean missingVals) {
    return getRandomList(
        length,
        missingVals,
        () -> {
          Float f = Float.NaN;
          while (f.isNaN()) {
            f = Float.intBitsToFloat(random().nextInt());
          }
          return f;
        });
  }

  private List<Integer> getRandomInts(int length, boolean missingVals, int boundPosNeg) {
    assertTrue(boundPosNeg > 0L);
    return getRandomList(
        length,
        missingVals,
        () -> (random().nextBoolean() ? 1 : -1) * random().nextInt(boundPosNeg));
  }

  private List<Integer> getRandomInts(int length, boolean missingVals) {
    return getRandomList(length, missingVals, () -> random().nextInt());
  }

  private List<Long> getRandomLongs(int length, boolean missingVals, long boundPosNeg) {
    assertTrue(boundPosNeg > 0L);
    return getRandomList(
        length,
        missingVals,
        () -> random().nextLong() % boundPosNeg); // see Random.nextInt(int bound)
  }

  private List<Long> getRandomLongs(int length, boolean missingVals) {
    return getRandomList(length, missingVals, () -> random().nextLong());
  }

  private List<Instant> getRandomInstants(int length, boolean missingVals) {
    return getRandomList(length, missingVals, () -> Instant.ofEpochMilli(random().nextLong()));
  }

  private String[] getSequentialStringArrayWithInts(int length) {
    String[] arr = new String[length];
    for (int i = 0; i < length; i++) {
      arr[i] = String.valueOf(i);
    }
    return arr;
  }

  private String[] getSequentialStringArrayWithDates(int length) {
    assertTrue(length < 60);
    String[] arr = new String[length];
    for (int i = 0; i < length; i++) {
      arr[i] = String.format(Locale.ROOT, "1995-12-11T19:59:%02dZ", i);
    }
    return arr;
  }

  private String[] getSequentialStringArrayWithDoubles(int length) {
    String[] arr = new String[length];
    for (int i = 0; i < length; i++) {
      arr[i] = String.format(Locale.ROOT, "%d.0", i);
    }
    return arr;
  }

  private void doTestFieldNotIndexed(String field, String[] values) throws IOException {
    assertTrue(values.length == 10);
    // test preconditions
    SchemaField sf = h.getCore().getLatestSchema().getField(field);
    assertFalse("Field should be indexed=false", sf.indexed());
    assertFalse("Field should be docValues=false", sf.hasDocValues());

    for (int i = 0; i < 10; i++) {
      assertU(adoc("id", String.valueOf(i), field, values[i]));
    }
    assertU(commit());
    assertQ(req("q", "*:*"), "//*[@numFound='10']");
    assertQ(
        "Can't search on index=false docValues=false field",
        req("q", field + ":[* TO *]"),
        "//*[@numFound='0']");
    h.getCore()
        .withSearcher(
            searcher -> {
              IndexReader ir = searcher.getIndexReader();
              assertEquals(
                  "Field " + field + " should have no point values",
                  0,
                  PointValues.size(ir, field));
              return null;
            });
  }

  private void doTestIntPointFieldExactQuery(final String field, final boolean testLong)
      throws Exception {
    doTestIntPointFieldExactQuery(field, testLong, true);
  }

  private String getTestString(boolean searchable, int numFound) {
    return "//*[@numFound='" + (searchable ? Integer.toString(numFound) : "0") + "']";
  }

  /**
   * @param field the field to use for indexing and searching against
   * @param testLong set to true if "field" is expected to support long values, false if only
   *     integers
   * @param searchable set to true if searches against "field" should succeed, false if field is
   *     only stored and searches should always get numFound=0
   */
  private void doTestIntPointFieldExactQuery(
      final String field, final boolean testLong, final boolean searchable) {
    int numValues = 10 * RANDOM_MULTIPLIER;
    Map<String, Integer> randCount = new HashMap<>(numValues);
    String[] rand =
        testLong
            ? toStringArray(getRandomLongs(numValues, false))
            : toStringArray(getRandomInts(numValues, false));
    for (int i = 0; i < numValues; i++) {
      randCount.merge(rand[i], 1, (a, b) -> a + b); // count unique values
      assertU(adoc("id", String.valueOf(i), field, rand[i]));
    }
    assertU(commit());

    for (int i = 0; i < numValues; i++) {
      assertQ(
          req(
              "q",
              field + ":" + (rand[i].startsWith("-") ? "\\" : "") + rand[i],
              "fl",
              "id," + field),
          getTestString(searchable, randCount.get(rand[i])));
    }

    StringBuilder builder = new StringBuilder();
    for (String value : randCount.keySet()) {
      if (builder.length() != 0) {
        builder.append(" OR ");
      }
      if (value.startsWith("-")) {
        builder.append("\\"); // escape negative sign
      }
      builder.append(value);
    }
    assertQ(
        req("debug", "true", "q", field + ":(" + builder + ")"),
        getTestString(searchable, numValues));

    assertU(
        adoc("id", String.valueOf(Integer.MAX_VALUE), field, String.valueOf(Integer.MAX_VALUE)));
    assertU(commit());
    assertQ(
        req("q", field + ":" + Integer.MAX_VALUE, "fl", "id, " + field),
        getTestString(searchable, 1));

    clearIndex();
    assertU(commit());
  }

  private void doTestPointFieldReturn(String field, String type, String[] values) {
    SchemaField sf = h.getCore().getLatestSchema().getField(field);
    assertTrue(
        "Unexpected field definition for " + field,
        sf.stored() || (sf.hasDocValues() && sf.useDocValuesAsStored()));
    for (int i = 0; i < values.length; i++) {
      assertU(adoc("id", String.valueOf(i), field, values[i]));
    }
    // Check using RTG
    if (Boolean.getBoolean("solr.index.updatelog.enabled")) {
      for (int i = 0; i < values.length; i++) {
        assertQ(
            req("qt", "/get", "id", String.valueOf(i)),
            "//doc/" + type + "[@name='" + field + "'][.='" + values[i] + "']");
      }
    }
    assertU(commit());
    String[] expected = new String[values.length + 1];
    expected[0] = "//*[@numFound='" + values.length + "']";
    for (int i = 0; i < values.length; i++) {
      expected[i + 1] =
          "//result/doc[str[@name='id']='"
              + i
              + "']/"
              + type
              + "[@name='"
              + field
              + "'][.='"
              + values[i]
              + "']";
    }
    assertQ(req("q", "*:*", "fl", "id, " + field, "rows", String.valueOf(values.length)), expected);

    // Check using RTG
    if (Boolean.getBoolean("solr.index.updatelog.enabled")) {
      for (int i = 0; i < values.length; i++) {
        assertQ(
            req("qt", "/get", "id", String.valueOf(i)),
            "//doc/" + type + "[@name='" + field + "'][.='" + values[i] + "']");
      }
    }
    clearIndex();
    assertU(commit());
  }

  private void doTestPointFieldNonSearchableRangeQuery(String fieldName, String... values) {
    for (int i = 9; i >= 0; i--) {
      SolrInputDocument doc = sdoc("id", String.valueOf(i));
      for (String value : values) {
        doc.addField(fieldName, value);
      }
      assertU(adoc(doc));
    }
    assertU(commit());
    assertQ(
        req("q", fieldName + ":[* TO *]", "fl", "id, " + fieldName, "sort", "id asc"),
        "//*[@numFound='0']");
  }

  private void doTestIntPointFieldRangeQuery(String fieldName, String type, boolean testLong) {
    for (int i = 9; i >= 0; i--) {
      assertU(adoc("id", String.valueOf(i), fieldName, String.valueOf(i)));
    }
    assertU(commit());
    assertQ(
        req("q", fieldName + ":[0 TO 3]", "fl", "id, " + fieldName, "sort", "id asc"),
        "//*[@numFound='4']",
        "//result/doc[1]/" + type + "[@name='" + fieldName + "'][.='0']",
        "//result/doc[2]/" + type + "[@name='" + fieldName + "'][.='1']",
        "//result/doc[3]/" + type + "[@name='" + fieldName + "'][.='2']",
        "//result/doc[4]/" + type + "[@name='" + fieldName + "'][.='3']");

    assertQ(
        req("q", fieldName + ":{0 TO 3]", "fl", "id, " + fieldName, "sort", "id asc"),
        "//*[@numFound='3']",
        "//result/doc[1]/" + type + "[@name='" + fieldName + "'][.='1']",
        "//result/doc[2]/" + type + "[@name='" + fieldName + "'][.='2']",
        "//result/doc[3]/" + type + "[@name='" + fieldName + "'][.='3']");

    assertQ(
        req("q", fieldName + ":[0 TO 3}", "fl", "id, " + fieldName, "sort", "id asc"),
        "//*[@numFound='3']",
        "//result/doc[1]/" + type + "[@name='" + fieldName + "'][.='0']",
        "//result/doc[2]/" + type + "[@name='" + fieldName + "'][.='1']",
        "//result/doc[3]/" + type + "[@name='" + fieldName + "'][.='2']");

    assertQ(
        req("q", fieldName + ":{0 TO 3}", "fl", "id, " + fieldName, "sort", "id asc"),
        "//*[@numFound='2']",
        "//result/doc[1]/" + type + "[@name='" + fieldName + "'][.='1']",
        "//result/doc[2]/" + type + "[@name='" + fieldName + "'][.='2']");

    assertQ(
        req("q", fieldName + ":{0 TO *}", "fl", "id, " + fieldName, "sort", "id asc"),
        "//*[@numFound='9']",
        "0=count(//result/doc/" + type + "[@name='" + fieldName + "'][.='0'])",
        "//result/doc[1]/" + type + "[@name='" + fieldName + "'][.='1']");

    assertQ(
        req("q", fieldName + ":{* TO 3}", "fl", "id, " + fieldName, "sort", "id desc"),
        "//*[@numFound='3']",
        "0=count(//result/doc/" + type + "[@name='" + fieldName + "'][.='3'])",
        "//result/doc[1]/" + type + "[@name='" + fieldName + "'][.='2']",
        "//result/doc[2]/" + type + "[@name='" + fieldName + "'][.='1']",
        "//result/doc[3]/" + type + "[@name='" + fieldName + "'][.='0']");

    assertQ(
        req("q", fieldName + ":[* TO 3}", "fl", "id, " + fieldName, "sort", "id desc"),
        "//*[@numFound='3']",
        "0=count(//result/doc/" + type + "[@name='" + fieldName + "'][.='3'])",
        "//result/doc[1]/" + type + "[@name='" + fieldName + "'][.='2']",
        "//result/doc[2]/" + type + "[@name='" + fieldName + "'][.='1']",
        "//result/doc[3]/" + type + "[@name='" + fieldName + "'][.='0']");

    assertQ(
        req("q", fieldName + ":[* TO *}", "fl", "id, " + fieldName, "sort", "id asc"),
        "//*[@numFound='10']",
        "//result/doc[1]/" + type + "[@name='" + fieldName + "'][.='0']",
        "//result/doc[10]/" + type + "[@name='" + fieldName + "'][.='9']");

    assertQ(
        req(
            "q",
            fieldName + ":[0 TO 1] OR " + fieldName + ":[8 TO 9]",
            "fl",
            "id, " + fieldName,
            "sort",
            "id asc"),
        "//*[@numFound='4']",
        "//result/doc[1]/" + type + "[@name='" + fieldName + "'][.='0']",
        "//result/doc[2]/" + type + "[@name='" + fieldName + "'][.='1']",
        "//result/doc[3]/" + type + "[@name='" + fieldName + "'][.='8']",
        "//result/doc[4]/" + type + "[@name='" + fieldName + "'][.='9']");

    assertQ(
        req("q", fieldName + ":[0 TO 1] AND " + fieldName + ":[1 TO 2]", "fl", "id, " + fieldName),
        "//*[@numFound='1']",
        "//result/doc[1]/" + type + "[@name='" + fieldName + "'][.='1']");

    assertQ(
        req(
            "q",
            fieldName + ":[0 TO 1] AND NOT " + fieldName + ":[1 TO 2]",
            "fl",
            "id, " + fieldName),
        "//*[@numFound='1']",
        "//result/doc[1]/" + type + "[@name='" + fieldName + "'][.='0']");

    clearIndex();
    assertU(commit());

    String[] arr;
    if (testLong) {
      arr = toAscendingStringArray(getRandomLongs(100, false), true);
    } else {
      arr = toAscendingStringArray(getRandomInts(100, false), true);
    }
    for (int i = 0; i < arr.length; i++) {
      assertU(adoc("id", String.valueOf(i), fieldName, arr[i]));
    }
    assertU(commit());
    for (int i = 0; i < arr.length; i++) {
      assertQ(
          req("q", fieldName + ":[" + arr[0] + " TO " + arr[i] + "]", "fl", "id, " + fieldName),
          "//*[@numFound='" + (i + 1) + "']");
      assertQ(
          req("q", fieldName + ":{" + arr[0] + " TO " + arr[i] + "}", "fl", "id, " + fieldName),
          "//*[@numFound='" + (Math.max(0, i - 1)) + "']");
      assertQ(
          req(
              "q",
              fieldName
                  + ":["
                  + arr[0]
                  + " TO "
                  + arr[i]
                  + "] AND "
                  + fieldName
                  + ":"
                  + arr[0].replace("-", "\\-"),
              "fl",
              "id, " + fieldName),
          "//*[@numFound='1']");
    }
    if (testLong) {
      assertQ(
          req(
              "q",
              fieldName + ":[" + Long.MIN_VALUE + " TO " + Long.MIN_VALUE + "}",
              "fl",
              "id, " + fieldName),
          "//*[@numFound='0']");
      assertQ(
          req(
              "q",
              fieldName + ":{" + Long.MAX_VALUE + " TO " + Long.MAX_VALUE + "]",
              "fl",
              "id, " + fieldName),
          "//*[@numFound='0']");
    } else {
      assertQ(
          req(
              "q",
              fieldName + ":[" + Integer.MIN_VALUE + " TO " + Integer.MIN_VALUE + "}",
              "fl",
              "id, " + fieldName),
          "//*[@numFound='0']");
      assertQ(
          req(
              "q",
              fieldName + ":{" + Integer.MAX_VALUE + " TO " + Integer.MAX_VALUE + "]",
              "fl",
              "id, " + fieldName),
          "//*[@numFound='0']");
    }
  }

  private void doTestPointFieldFacetField(
      String nonDocValuesField, String docValuesField, String[] numbers) {
    assertTrue(numbers != null && numbers.length == 10);

    assertFalse(h.getCore().getLatestSchema().getField(docValuesField).multiValued());
    assertTrue(h.getCore().getLatestSchema().getField(docValuesField).hasDocValues());
    assertTrue(
        h.getCore().getLatestSchema().getField(docValuesField).getType() instanceof PointField);

    for (int i = 0; i < 10; i++) {
      assertU(
          adoc("id", String.valueOf(i), docValuesField, numbers[i], nonDocValuesField, numbers[i]));
    }
    assertU(commit());
    assertQ(
        req(
            "q",
            "*:*",
            "fl",
            "id, " + docValuesField,
            "facet",
            "true",
            "facet.field",
            docValuesField),
        "//*[@numFound='10']",
        "//lst[@name='facet_counts']/lst[@name='facet_fields']/lst[@name='"
            + docValuesField
            + "']/int[@name='"
            + numbers[1]
            + "'][.='1']",
        "//lst[@name='facet_counts']/lst[@name='facet_fields']/lst[@name='"
            + docValuesField
            + "']/int[@name='"
            + numbers[2]
            + "'][.='1']",
        "//lst[@name='facet_counts']/lst[@name='facet_fields']/lst[@name='"
            + docValuesField
            + "']/int[@name='"
            + numbers[3]
            + "'][.='1']");

    assertU(adoc("id", "10", docValuesField, numbers[1], nonDocValuesField, numbers[1]));

    assertU(commit());
    assertQ(
        req(
            "q",
            "*:*",
            "fl",
            "id, " + docValuesField,
            "facet",
            "true",
            "facet.field",
            docValuesField),
        "//*[@numFound='11']",
        "//lst[@name='facet_counts']/lst[@name='facet_fields']/lst[@name='"
            + docValuesField
            + "']/int[@name='"
            + numbers[1]
            + "'][.='2']",
        "//lst[@name='facet_counts']/lst[@name='facet_fields']/lst[@name='"
            + docValuesField
            + "']/int[@name='"
            + numbers[2]
            + "'][.='1']",
        "//lst[@name='facet_counts']/lst[@name='facet_fields']/lst[@name='"
            + docValuesField
            + "']/int[@name='"
            + numbers[3]
            + "'][.='1']");

    assertFalse(h.getCore().getLatestSchema().getField(nonDocValuesField).hasDocValues());
    assertTrue(
        h.getCore().getLatestSchema().getField(nonDocValuesField).getType() instanceof PointField);
    assertQEx(
        "Expecting Exception",
        "Can't facet on a PointField without docValues",
        req(
            "q",
            "*:*",
            "fl",
            "id, " + nonDocValuesField,
            "facet",
            "true",
            "facet.field",
            nonDocValuesField),
        SolrException.ErrorCode.BAD_REQUEST);
  }

  private void doTestIntPointFunctionQuery(String field) {
    assertTrue(h.getCore().getLatestSchema().getField(field).getType() instanceof PointField);
    int numVals = 10 * RANDOM_MULTIPLIER;
    List<Integer> values = getRandomInts(numVals, false);
    String assertNumFound = "//*[@numFound='" + numVals + "']";
    String[] idAscXpathChecks = new String[numVals + 1];
    String[] idAscNegXpathChecks = new String[numVals + 1];
    idAscXpathChecks[0] = assertNumFound;
    idAscNegXpathChecks[0] = assertNumFound;
    for (int i = 0; i < values.size(); ++i) {
      assertU(
          adoc(
              "id",
              Character.valueOf((char) ('A' + i)).toString(),
              field,
              String.valueOf(values.get(i))));
      // reminder: xpath array indexes start at 1
      idAscXpathChecks[i + 1] =
          "//result/doc["
              + (1 + i)
              + "]/int[@name='field("
              + field
              + ")'][.='"
              + values.get(i)
              + "']";
      idAscNegXpathChecks[i + 1] =
          "//result/doc["
              + (1 + i)
              + "]/float[@name='product(-1,"
              + field
              + ")'][.='"
              + (-1.0f * (float) values.get(i))
              + "']";
    }
    assertU(commit());
    assertQ(
        req(
            "q",
            "*:*",
            "fl",
            "id, " + field + ", field(" + field + ")",
            "rows",
            String.valueOf(numVals),
            "sort",
            "id asc"),
        idAscXpathChecks);
    assertQ(
        req(
            "q",
            "*:*",
            "fl",
            "id, " + field + ", product(-1," + field + ")",
            "rows",
            String.valueOf(numVals),
            "sort",
            "id asc"),
        idAscNegXpathChecks);

    List<PosVal<Integer>> ascNegPosVals =
        toAscendingPosVals(values.stream().map(v -> -v).collect(Collectors.toList()), true);
    String[] ascNegXpathChecks = new String[numVals + 1];
    ascNegXpathChecks[0] = assertNumFound;
    for (int i = 0; i < ascNegPosVals.size(); ++i) {
      PosVal<Integer> posVal = ascNegPosVals.get(i);
      ascNegXpathChecks[i + 1] =
          "//result/doc["
              + (1 + i)
              + "]/int[@name='"
              + field
              + "'][.='"
              + values.get(posVal.pos)
              + "']";
    }
    assertQ(
        req(
            "q",
            "*:*",
            "fl",
            "id, " + field,
            "rows",
            String.valueOf(numVals),
            "sort",
            "product(-1," + field + ") asc"),
        ascNegXpathChecks);

    clearIndex();
    assertU(commit());
  }

  private void doTestLongPointFunctionQuery(String field) {
    assertTrue(h.getCore().getLatestSchema().getField(field).getType() instanceof PointField);
    int numVals = 10 * RANDOM_MULTIPLIER;
    List<Long> values = getRandomLongs(numVals, false);
    String assertNumFound = "//*[@numFound='" + numVals + "']";
    String[] idAscXpathChecks = new String[numVals + 1];
    String[] idAscNegXpathChecks = new String[numVals + 1];
    idAscXpathChecks[0] = assertNumFound;
    idAscNegXpathChecks[0] = assertNumFound;
    for (int i = 0; i < values.size(); ++i) {
      assertU(
          adoc(
              "id",
              Character.valueOf((char) ('A' + i)).toString(),
              field,
              String.valueOf(values.get(i))));
      // reminder: xpath array indexes start at 1
      idAscXpathChecks[i + 1] =
          "//result/doc["
              + (1 + i)
              + "]/long[@name='field("
              + field
              + ")'][.='"
              + values.get(i)
              + "']";
      idAscNegXpathChecks[i + 1] =
          "//result/doc["
              + (1 + i)
              + "]/float[@name='product(-1,"
              + field
              + ")'][.='"
              + (-1.0f * (float) values.get(i))
              + "']";
    }
    assertU(commit());
    assertQ(
        req(
            "q",
            "*:*",
            "fl",
            "id, " + field + ", field(" + field + ")",
            "rows",
            String.valueOf(numVals),
            "sort",
            "id asc"),
        idAscXpathChecks);
    assertQ(
        req(
            "q",
            "*:*",
            "fl",
            "id, " + field + ", product(-1," + field + ")",
            "rows",
            String.valueOf(numVals),
            "sort",
            "id asc"),
        idAscNegXpathChecks);

    List<PosVal<Long>> ascNegPosVals =
        toAscendingPosVals(values.stream().map(v -> -v).collect(Collectors.toList()), true);
    String[] ascNegXpathChecks = new String[numVals + 1];
    ascNegXpathChecks[0] = assertNumFound;
    for (int i = 0; i < ascNegPosVals.size(); ++i) {
      PosVal<Long> posVal = ascNegPosVals.get(i);
      ascNegXpathChecks[i + 1] =
          "//result/doc["
              + (1 + i)
              + "]/long[@name='"
              + field
              + "'][.='"
              + values.get(posVal.pos)
              + "']";
    }
    assertQ(
        req(
            "q",
            "*:*",
            "fl",
            "id, " + field,
            "rows",
            String.valueOf(numVals),
            "sort",
            "product(-1," + field + ") asc"),
        ascNegXpathChecks);

    clearIndex();
    assertU(commit());
  }

  /**
   * Checks that the specified field can not be used as a value source, even if there are documents
   * with (all) the specified values in the index.
   *
   * @param field the field name to try and sort on
   * @param errSubStr substring to look for in the error msg
   * @param values one or more values to put into the doc(s) in the index - may be more than one for
   *     multivalued fields
   */
  private void doTestPointFieldFunctionQueryError(
      String field, String errSubStr, String... values) {
    final int numDocs = atLeast(random(), 10);
    for (int i = 0; i < numDocs; i++) {
      SolrInputDocument doc = sdoc("id", String.valueOf(i));
      for (String v : values) {
        doc.addField(field, v);
      }
      assertU(adoc(doc));
    }

    assertQEx(
        "Should not be able to use field in function: " + field,
        errSubStr,
        req("q", "*:*", "fl", "id", "fq", "{!frange l=0 h=100}product(-1, " + field + ")"),
        SolrException.ErrorCode.BAD_REQUEST);

    clearIndex();
    assertU(commit());

    // empty index should (also) give same error
    assertQEx(
        "Should not be able to use field in function: " + field,
        errSubStr,
        req("q", "*:*", "fl", "id", "fq", "{!frange l=0 h=100}product(-1, " + field + ")"),
        SolrException.ErrorCode.BAD_REQUEST);
  }

  private void doTestPointStats(
      String field,
      String dvField,
      String[] numbers,
      double min,
      double max,
      int count,
      int missing,
      double delta) {
    String minMin = String.valueOf(min - Math.abs(delta * min));
    String maxMin = String.valueOf(min + Math.abs(delta * min));
    String minMax = String.valueOf(max - Math.abs(delta * max));
    String maxMax = String.valueOf(max + Math.abs(delta * max));
    for (int i = 0; i < numbers.length; i++) {
      assertU(adoc("id", String.valueOf(i), dvField, numbers[i], field, numbers[i]));
    }
    assertU(adoc("id", String.valueOf(numbers.length)));
    assertU(commit());
    assertTrue(h.getCore().getLatestSchema().getField(dvField).hasDocValues());
    assertTrue(h.getCore().getLatestSchema().getField(dvField).getType() instanceof PointField);
    assertQ(
        req("q", "*:*", "fl", "id, " + dvField, "stats", "true", "stats.field", dvField),
        "//*[@numFound='" + (numbers.length + 1) + "']",
        "//lst[@name='stats']/lst[@name='stats_fields']/lst[@name='"
            + dvField
            + "']/double[@name='min'][.>="
            + minMin
            + "]",
        "//lst[@name='stats']/lst[@name='stats_fields']/lst[@name='"
            + dvField
            + "']/double[@name='min'][.<="
            + maxMin
            + "]",
        "//lst[@name='stats']/lst[@name='stats_fields']/lst[@name='"
            + dvField
            + "']/double[@name='max'][.>="
            + minMax
            + "]",
        "//lst[@name='stats']/lst[@name='stats_fields']/lst[@name='"
            + dvField
            + "']/double[@name='max'][.<="
            + maxMax
            + "]",
        "//lst[@name='stats']/lst[@name='stats_fields']/lst[@name='"
            + dvField
            + "']/long[@name='count'][.='"
            + count
            + "']",
        "//lst[@name='stats']/lst[@name='stats_fields']/lst[@name='"
            + dvField
            + "']/long[@name='missing'][.='"
            + missing
            + "']");

    assertFalse(h.getCore().getLatestSchema().getField(field).hasDocValues());
    assertTrue(h.getCore().getLatestSchema().getField(field).getType() instanceof PointField);
    assertQEx(
        "Expecting Exception",
        "Can't calculate stats on a PointField without docValues",
        req("q", "*:*", "fl", "id, " + field, "stats", "true", "stats.field", field),
        SolrException.ErrorCode.BAD_REQUEST);
  }

  private void doTestPointFieldMultiValuedExactQuery(final String fieldName, final String[] numbers)
      throws Exception {
    doTestPointFieldMultiValuedExactQuery(fieldName, numbers, true);
  }

  /**
   * @param fieldName the field to use for indexing and searching against
   * @param numbers list of 20 values to index in 10 docs (pairwise)
   * @param searchable set to true if searches against "field" should succeed, false if field is
   *     only stored and searches should always get numFound=0
   */
  private void doTestPointFieldMultiValuedExactQuery(
      final String fieldName, final String[] numbers, final boolean searchable) {

    final String MATCH_ONE = "//*[@numFound='" + (searchable ? "1" : "0") + "']";
    final String MATCH_TWO = "//*[@numFound='" + (searchable ? "2" : "0") + "']";

    assertTrue(numbers != null && numbers.length == 20);
    assertTrue(h.getCore().getLatestSchema().getField(fieldName).multiValued());
    assertTrue(h.getCore().getLatestSchema().getField(fieldName).getType() instanceof PointField);
    for (int i = 0; i < 10; i++) {
      assertU(adoc("id", String.valueOf(i), fieldName, numbers[i], fieldName, numbers[i + 10]));
    }
    assertU(commit());
    FieldType type = h.getCore().getLatestSchema().getField(fieldName).getType();
    for (int i = 0; i < 20; i++) {
      if (type instanceof DatePointField) {
        assertQ(req("q", fieldName + ":\"" + numbers[i] + "\""), MATCH_ONE);
      } else {
        assertQ(req("q", fieldName + ":" + numbers[i].replace("-", "\\-")), MATCH_ONE);
      }
    }

    for (int i = 0; i < 20; i++) {
      if (type instanceof DatePointField) {
        assertQ(
            req(
                "q",
                fieldName
                    + ":\""
                    + numbers[i]
                    + "\""
                    + " OR "
                    + fieldName
                    + ":\""
                    + numbers[(i + 1) % 10]
                    + "\""),
            MATCH_TWO);
      } else {
        assertQ(
            req(
                "q",
                fieldName
                    + ":"
                    + numbers[i].replace("-", "\\-")
                    + " OR "
                    + fieldName
                    + ":"
                    + numbers[(i + 1) % 10].replace("-", "\\-")),
            MATCH_TWO);
      }
    }
  }

  private void doTestPointFieldMultiValuedReturn(String fieldName, String type, String[] numbers) {
    assertTrue(numbers != null && numbers.length == 20);
    assertTrue(h.getCore().getLatestSchema().getField(fieldName).multiValued());
    assertTrue(h.getCore().getLatestSchema().getField(fieldName).getType() instanceof PointField);
    for (int i = 9; i >= 0; i--) {
      assertU(adoc("id", String.valueOf(i), fieldName, numbers[i], fieldName, numbers[i + 10]));
    }
    // Check using RTG before commit
    if (Boolean.getBoolean("solr.index.updatelog.enabled")) {
      for (int i = 0; i < 10; i++) {
        assertQ(
            req("qt", "/get", "id", String.valueOf(i)),
            "//doc/arr[@name='" + fieldName + "']/" + type + "[.='" + numbers[i] + "']",
            "//doc/arr[@name='" + fieldName + "']/" + type + "[.='" + numbers[i + 10] + "']",
            "count(//doc/arr[@name='" + fieldName + "']/" + type + ")=2");
      }
    }
    // Check using RTG after commit
    assertU(commit());
    if (Boolean.getBoolean("solr.index.updatelog.enabled")) {
      for (int i = 0; i < 10; i++) {
        assertQ(
            req("qt", "/get", "id", String.valueOf(i)),
            "//doc/arr[@name='" + fieldName + "']/" + type + "[.='" + numbers[i] + "']",
            "//doc/arr[@name='" + fieldName + "']/" + type + "[.='" + numbers[i + 10] + "']",
            "count(//doc/arr[@name='" + fieldName + "']/" + type + ")=2");
      }
    }
    String[] expected = new String[21];
    expected[0] = "//*[@numFound='10']";
    for (int i = 1; i <= 10; i++) {
      // checks for each doc's two values aren't next to each other in array, but that doesn't
      // matter
      // for correctness
      expected[i] =
          "//result/doc["
              + i
              + "]/arr[@name='"
              + fieldName
              + "']/"
              + type
              + "[.='"
              + numbers[i - 1]
              + "']";
      expected[i + 10] =
          "//result/doc["
              + i
              + "]/arr[@name='"
              + fieldName
              + "']/"
              + type
              + "[.='"
              + numbers[i + 9]
              + "']";
    }
    assertQ(req("q", "*:*", "fl", "id, " + fieldName, "sort", "id asc"), expected);
  }

  private void doTestPointFieldMultiValuedRangeQuery(
      String fieldName, String type, String[] numbers) {
    assertTrue(numbers != null && numbers.length == 20);
    SchemaField sf = h.getCore().getLatestSchema().getField(fieldName);
    assertTrue(sf.multiValued());
    assertTrue(sf.getType() instanceof PointField);
    for (int i = 9; i >= 0; i--) {
      assertU(adoc("id", String.valueOf(i), fieldName, numbers[i], fieldName, numbers[i + 10]));
    }
    assertU(commit());
    assertQ(
        req(
            "q",
            String.format(Locale.ROOT, "%s:[%s TO %s]", fieldName, numbers[0], numbers[3]),
            "fl",
            "id, " + fieldName,
            "sort",
            "id asc"),
        "//*[@numFound='4']",
        "//result/doc[1]/arr[@name='" + fieldName + "']/" + type + "[1][.='" + numbers[0] + "']",
        "//result/doc[1]/arr[@name='" + fieldName + "']/" + type + "[2][.='" + numbers[10] + "']",
        "//result/doc[2]/arr[@name='" + fieldName + "']/" + type + "[1][.='" + numbers[1] + "']",
        "//result/doc[2]/arr[@name='" + fieldName + "']/" + type + "[2][.='" + numbers[11] + "']",
        "//result/doc[3]/arr[@name='" + fieldName + "']/" + type + "[1][.='" + numbers[2] + "']",
        "//result/doc[3]/arr[@name='" + fieldName + "']/" + type + "[2][.='" + numbers[12] + "']",
        "//result/doc[4]/arr[@name='" + fieldName + "']/" + type + "[1][.='" + numbers[3] + "']",
        "//result/doc[4]/arr[@name='" + fieldName + "']/" + type + "[2][.='" + numbers[13] + "']");

    assertQ(
        req(
            "q",
            String.format(Locale.ROOT, "%s:{%s TO %s]", fieldName, numbers[0], numbers[3]),
            "fl",
            "id, " + fieldName,
            "sort",
            "id asc"),
        "//*[@numFound='3']",
        "//result/doc[1]/arr[@name='" + fieldName + "']/" + type + "[1][.='" + numbers[1] + "']",
        "//result/doc[2]/arr[@name='" + fieldName + "']/" + type + "[1][.='" + numbers[2] + "']",
        "//result/doc[3]/arr[@name='" + fieldName + "']/" + type + "[1][.='" + numbers[3] + "']");

    assertQ(
        req(
            "q",
            String.format(Locale.ROOT, "%s:[%s TO %s}", fieldName, numbers[0], numbers[3]),
            "fl",
            "id, " + fieldName,
            "sort",
            "id asc"),
        "//*[@numFound='3']",
        "//result/doc[1]/arr[@name='" + fieldName + "']/" + type + "[1][.='" + numbers[0] + "']",
        "//result/doc[2]/arr[@name='" + fieldName + "']/" + type + "[1][.='" + numbers[1] + "']",
        "//result/doc[3]/arr[@name='" + fieldName + "']/" + type + "[1][.='" + numbers[2] + "']");

    assertQ(
        req(
            "q",
            String.format(Locale.ROOT, "%s:{%s TO %s}", fieldName, numbers[0], numbers[3]),
            "fl",
            "id, " + fieldName,
            "sort",
            "id asc"),
        "//*[@numFound='2']",
        "//result/doc[1]/arr[@name='" + fieldName + "']/" + type + "[1][.='" + numbers[1] + "']",
        "//result/doc[2]/arr[@name='" + fieldName + "']/" + type + "[1][.='" + numbers[2] + "']");

    assertQ(
        req(
            "q",
            String.format(Locale.ROOT, "%s:{%s TO *}", fieldName, numbers[0]),
            "fl",
            "id, " + fieldName,
            "sort",
            "id asc"),
        "//*[@numFound='10']",
        "//result/doc[1]/arr[@name='" + fieldName + "']/" + type + "[1][.='" + numbers[0] + "']");

    assertQ(
        req(
            "q",
            String.format(Locale.ROOT, "%s:{%s TO *}", fieldName, numbers[10]),
            "fl",
            "id, " + fieldName,
            "sort",
            "id asc"),
        "//*[@numFound='9']",
        "//result/doc[1]/arr[@name='" + fieldName + "']/" + type + "[1][.='" + numbers[1] + "']");

    assertQ(
        req(
            "q",
            String.format(Locale.ROOT, "%s:{* TO %s}", fieldName, numbers[3]),
            "fl",
            "id, " + fieldName,
            "sort",
            "id asc"),
        "//*[@numFound='3']",
        "//result/doc[1]/arr[@name='" + fieldName + "']/" + type + "[1][.='" + numbers[0] + "']");

    assertQ(
        req(
            "q",
            String.format(Locale.ROOT, "%s:[* TO %s}", fieldName, numbers[3]),
            "fl",
            "id, " + fieldName,
            "sort",
            "id asc"),
        "//*[@numFound='3']",
        "//result/doc[1]/arr[@name='" + fieldName + "']/" + type + "[1][.='" + numbers[0] + "']");

    assertQ(
        req("q", fieldName + ":[* TO *}", "fl", "id, " + fieldName, "sort", "id asc"),
        "//*[@numFound='10']",
        "//result/doc[1]/arr[@name='" + fieldName + "']/" + type + "[1][.='" + numbers[0] + "']",
        "//result/doc[10]/arr[@name='" + fieldName + "']/" + type + "[1][.='" + numbers[9] + "']");

    assertQ(
        req(
            "q",
            String.format(
                Locale.ROOT,
                "%s:[%s TO %s] OR %s:[%s TO %s]",
                fieldName,
                numbers[0],
                numbers[1],
                fieldName,
                numbers[8],
                numbers[9]),
            "fl",
            "id, " + fieldName,
            "sort",
            "id asc"),
        "//*[@numFound='4']",
        "//result/doc[1]/arr[@name='" + fieldName + "']/" + type + "[1][.='" + numbers[0] + "']",
        "//result/doc[2]/arr[@name='" + fieldName + "']/" + type + "[1][.='" + numbers[1] + "']",
        "//result/doc[3]/arr[@name='" + fieldName + "']/" + type + "[1][.='" + numbers[8] + "']",
        "//result/doc[4]/arr[@name='" + fieldName + "']/" + type + "[1][.='" + numbers[9] + "']");

    assertQ(
        req(
            "q",
            String.format(
                Locale.ROOT,
                "%s:[%s TO %s] OR %s:[%s TO %s]",
                fieldName,
                numbers[0],
                numbers[0],
                fieldName,
                numbers[10],
                numbers[10]),
            "fl",
            "id, " + fieldName,
            "sort",
            "id asc"),
        "//*[@numFound='1']",
        "//result/doc[1]/arr[@name='" + fieldName + "']/" + type + "[1][.='" + numbers[0] + "']");

    if (sf.getType().getNumberType() == NumberType.FLOAT
        || sf.getType().getNumberType() == NumberType.DOUBLE) {
      doTestDoubleFloatRangeLimits(fieldName, sf.getType().getNumberType() == NumberType.DOUBLE);
    }
  }

  private void doTestPointFieldMultiValuedFacetField(
      String nonDocValuesField, String dvFieldName, String[] numbers) {
    assertTrue(numbers != null && numbers.length == 20);
    assertTrue(h.getCore().getLatestSchema().getField(dvFieldName).multiValued());
    assertTrue(h.getCore().getLatestSchema().getField(dvFieldName).hasDocValues());
    assertTrue(h.getCore().getLatestSchema().getField(dvFieldName).getType() instanceof PointField);

    for (int i = 0; i < 10; i++) {
      assertU(
          adoc(
              "id",
              String.valueOf(i),
              dvFieldName,
              numbers[i],
              dvFieldName,
              numbers[i + 10],
              nonDocValuesField,
              numbers[i],
              nonDocValuesField,
              numbers[i + 10]));
      if (rarely()) {
        assertU(commit());
      }
    }
    assertU(commit());

    assertQ(
        req("q", "*:*", "fl", "id, " + dvFieldName, "facet", "true", "facet.field", dvFieldName),
        "//*[@numFound='10']",
        "//lst[@name='facet_counts']/lst[@name='facet_fields']/lst[@name='"
            + dvFieldName
            + "']/int[@name='"
            + numbers[1]
            + "'][.='1']",
        "//lst[@name='facet_counts']/lst[@name='facet_fields']/lst[@name='"
            + dvFieldName
            + "']/int[@name='"
            + numbers[2]
            + "'][.='1']",
        "//lst[@name='facet_counts']/lst[@name='facet_fields']/lst[@name='"
            + dvFieldName
            + "']/int[@name='"
            + numbers[3]
            + "'][.='1']",
        "//lst[@name='facet_counts']/lst[@name='facet_fields']/lst[@name='"
            + dvFieldName
            + "']/int[@name='"
            + numbers[10]
            + "'][.='1']",
        "//lst[@name='facet_counts']/lst[@name='facet_fields']/lst[@name='"
            + dvFieldName
            + "']/int[@name='"
            + numbers[11]
            + "'][.='1']",
        "//lst[@name='facet_counts']/lst[@name='facet_fields']/lst[@name='"
            + dvFieldName
            + "']/int[@name='"
            + numbers[12]
            + "'][.='1']");

    assertU(adoc("id", "10", dvFieldName, numbers[1], nonDocValuesField, numbers[1]));

    assertU(commit());
    assertQ(
        req("q", "*:*", "fl", "id, " + dvFieldName, "facet", "true", "facet.field", dvFieldName),
        "//*[@numFound='11']",
        "//lst[@name='facet_counts']/lst[@name='facet_fields']/lst[@name='"
            + dvFieldName
            + "']/int[@name='"
            + numbers[1]
            + "'][.='2']",
        "//lst[@name='facet_counts']/lst[@name='facet_fields']/lst[@name='"
            + dvFieldName
            + "']/int[@name='"
            + numbers[2]
            + "'][.='1']",
        "//lst[@name='facet_counts']/lst[@name='facet_fields']/lst[@name='"
            + dvFieldName
            + "']/int[@name='"
            + numbers[3]
            + "'][.='1']",
        "//lst[@name='facet_counts']/lst[@name='facet_fields']/lst[@name='"
            + dvFieldName
            + "']/int[@name='"
            + numbers[10]
            + "'][.='1']");

    assertU(
        adoc(
            "id",
            "10",
            dvFieldName,
            numbers[1],
            nonDocValuesField,
            numbers[1],
            dvFieldName,
            numbers[1],
            nonDocValuesField,
            numbers[1]));
    assertU(commit());
    assertQ(
        req(
            "q",
            "*:*",
            "fl",
            "id, " + dvFieldName,
            "facet",
            "true",
            "facet.field",
            dvFieldName,
            "facet.missing",
            "true"),
        "//*[@numFound='11']",
        "//lst[@name='facet_counts']/lst[@name='facet_fields']/lst[@name='"
            + dvFieldName
            + "']/int[@name='"
            + numbers[1]
            + "'][.='2']",
        "//lst[@name='facet_counts']/lst[@name='facet_fields']/lst[@name='"
            + dvFieldName
            + "']/int[@name='"
            + numbers[2]
            + "'][.='1']",
        "//lst[@name='facet_counts']/lst[@name='facet_fields']/lst[@name='"
            + dvFieldName
            + "']/int[@name='"
            + numbers[3]
            + "'][.='1']",
        "//lst[@name='facet_counts']/lst[@name='facet_fields']/lst[@name='"
            + dvFieldName
            + "']/int[@name='"
            + numbers[10]
            + "'][.='1']",
        "//lst[@name='facet_counts']/lst[@name='facet_fields']/lst[@name='"
            + dvFieldName
            + "']/int[not(@name)][.='0']");

    assertU(adoc("id", "10")); // add missing values
    assertU(commit());
    assertQ(
        req(
            "q",
            "*:*",
            "fl",
            "id, " + dvFieldName,
            "facet",
            "true",
            "facet.field",
            dvFieldName,
            "facet.missing",
            "true"),
        "//*[@numFound='11']",
        "//lst[@name='facet_counts']/lst[@name='facet_fields']/lst[@name='"
            + dvFieldName
            + "']/int[@name='"
            + numbers[1]
            + "'][.='1']",
        "//lst[@name='facet_counts']/lst[@name='facet_fields']/lst[@name='"
            + dvFieldName
            + "']/int[@name='"
            + numbers[2]
            + "'][.='1']",
        "//lst[@name='facet_counts']/lst[@name='facet_fields']/lst[@name='"
            + dvFieldName
            + "']/int[@name='"
            + numbers[3]
            + "'][.='1']",
        "//lst[@name='facet_counts']/lst[@name='facet_fields']/lst[@name='"
            + dvFieldName
            + "']/int[@name='"
            + numbers[10]
            + "'][.='1']",
        "//lst[@name='facet_counts']/lst[@name='facet_fields']/lst[@name='"
            + dvFieldName
            + "']/int[not(@name)][.='1']");

    assertQ(
        req(
            "q",
            "*:*",
            "fl",
            "id, " + dvFieldName,
            "facet",
            "true",
            "facet.field",
            dvFieldName,
            "facet.mincount",
            "3"),
        "//*[@numFound='11']",
        "count(//lst[@name='facet_counts']/lst[@name='facet_fields']/lst[@name='"
            + dvFieldName
            + "']/int)=0");

    assertQ(
        req("q", "id:0", "fl", "id, " + dvFieldName, "facet", "true", "facet.field", dvFieldName),
        "//*[@numFound='1']",
        "//lst[@name='facet_counts']/lst[@name='facet_fields']/lst[@name='"
            + dvFieldName
            + "']/int[@name='"
            + numbers[0]
            + "'][.='1']",
        "//lst[@name='facet_counts']/lst[@name='facet_fields']/lst[@name='"
            + dvFieldName
            + "']/int[@name='"
            + numbers[10]
            + "'][.='1']",
        "count(//lst[@name='facet_counts']/lst[@name='facet_fields']/lst[@name='"
            + dvFieldName
            + "']/int)=2");

    assertFalse(h.getCore().getLatestSchema().getField(nonDocValuesField).hasDocValues());
    assertTrue(
        h.getCore().getLatestSchema().getField(nonDocValuesField).getType() instanceof PointField);
    assertQEx(
        "Expecting Exception",
        "Can't facet on a PointField without docValues",
        req(
            "q",
            "*:*",
            "fl",
            "id, " + nonDocValuesField,
            "facet",
            "true",
            "facet.field",
            nonDocValuesField),
        SolrException.ErrorCode.BAD_REQUEST);
    clearIndex();
    assertU(commit());

    String smaller, larger;
    try {
      if (Long.parseLong(numbers[1]) < Long.parseLong(numbers[2])) {
        smaller = numbers[1];
        larger = numbers[2];
      } else {
        smaller = numbers[2];
        larger = numbers[1];
      }
    } catch (NumberFormatException e) {
      try {
        if (Double.parseDouble(numbers[1]) < Double.parseDouble(numbers[2])) {
          smaller = numbers[1];
          larger = numbers[2];
        } else {
          smaller = numbers[2];
          larger = numbers[1];
        }
      } catch (NumberFormatException e2) {
        if (DateMathParser.parseMath(null, numbers[1]).getTime()
            < DateMathParser.parseMath(null, numbers[2]).getTime()) {
          smaller = numbers[1];
          larger = numbers[2];
        } else {
          smaller = numbers[2];
          larger = numbers[1];
        }
      }
    }

    assertU(adoc("id", "1", dvFieldName, smaller, dvFieldName, larger));
    assertU(adoc("id", "2", dvFieldName, larger));
    assertU(commit());

    assertQ(
        req("q", "*:*", "fl", "id, " + dvFieldName, "facet", "true", "facet.field", dvFieldName),
        "//*[@numFound='2']",
        "//lst[@name='facet_counts']/lst[@name='facet_fields']/lst[@name='"
            + dvFieldName
            + "']/int[@name='"
            + larger
            + "'][.='2']",
        "//lst[@name='facet_counts']/lst[@name='facet_fields']/lst[@name='"
            + dvFieldName
            + "']/int[@name='"
            + smaller
            + "'][.='1']",
        "count(//lst[@name='facet_counts']/lst[@name='facet_fields']/lst[@name='"
            + dvFieldName
            + "']/int)=2");

    assertQ(
        req(
            "q",
            "*:*",
            "fl",
            "id, " + dvFieldName,
            "facet",
            "true",
            "facet.field",
            dvFieldName,
            "facet.sort",
            "index"),
        "//*[@numFound='2']",
        "//lst[@name='facet_counts']/lst[@name='facet_fields']/lst[@name='"
            + dvFieldName
            + "']/int[@name='"
            + smaller
            + "'][.='1']",
        "//lst[@name='facet_counts']/lst[@name='facet_fields']/lst[@name='"
            + dvFieldName
            + "']/int[@name='"
            + larger
            + "'][.='2']",
        "count(//lst[@name='facet_counts']/lst[@name='facet_fields']/lst[@name='"
            + dvFieldName
            + "']/int)=2");

    clearIndex();
    assertU(commit());
  }

  private void doTestPointMultiValuedFunctionQuery(
      String nonDocValuesField, String docValuesField, String[] numbers) {
    assertTrue(numbers != null && numbers.length == 20);
    for (int i = 0; i < 10; i++) {
      assertU(
          adoc(
              "id",
              String.valueOf(i),
              docValuesField,
              numbers[i],
              docValuesField,
              numbers[i + 10],
              nonDocValuesField,
              numbers[i],
              nonDocValuesField,
              numbers[i + 10]));
    }
    assertU(commit());
    assertTrue(h.getCore().getLatestSchema().getField(docValuesField).hasDocValues());
    assertTrue(h.getCore().getLatestSchema().getField(docValuesField).multiValued());
    assertTrue(
        h.getCore().getLatestSchema().getField(docValuesField).getType() instanceof PointField);
    String function = "field(" + docValuesField + ", min)";

    assertQ(
        req("q", "*:*", "fl", "id, " + docValuesField, "sort", function + " desc"),
        "//*[@numFound='10']",
        "//result/doc[1]/str[@name='id'][.='9']",
        "//result/doc[2]/str[@name='id'][.='8']",
        "//result/doc[3]/str[@name='id'][.='7']",
        "//result/doc[10]/str[@name='id'][.='0']");

    assertFalse(h.getCore().getLatestSchema().getField(nonDocValuesField).hasDocValues());
    assertTrue(h.getCore().getLatestSchema().getField(nonDocValuesField).multiValued());
    assertTrue(
        h.getCore().getLatestSchema().getField(nonDocValuesField).getType() instanceof PointField);

    function = "field(" + nonDocValuesField + ",min)";

    assertQEx(
        "Expecting Exception",
        "sort param could not be parsed as a query",
        req("q", "*:*", "fl", "id", "sort", function + " desc"),
        SolrException.ErrorCode.BAD_REQUEST);

    assertQEx(
        "Expecting Exception",
        "docValues='true' is required to select 'min' value from multivalued field ("
            + nonDocValuesField
            + ") at query time",
        req("q", "*:*", "fl", "id, " + function),
        SolrException.ErrorCode.BAD_REQUEST);

    function = "field(" + docValuesField + ",foo)";
    assertQEx(
        "Expecting Exception",
        "Multi-Valued field selector 'foo' not supported",
        req("q", "*:*", "fl", "id, " + function),
        SolrException.ErrorCode.BAD_REQUEST);
  }

  private void doTestMultiValuedPointFieldsAtomicUpdates(
      String field, String type, String[] values) {
    assertEquals(3, values.length);
    assertU(adoc(sdoc("id", "1", field, String.valueOf(values[0]))));
    assertU(commit());

    assertQ(
        req("q", "id:1"),
        "//result/doc[1]/arr[@name='" + field + "']/" + type + "[.='" + values[0] + "']",
        "count(//result/doc[1]/arr[@name='" + field + "']/" + type + ")=1");

    assertU(adoc(sdoc("id", "1", field, Map.of("add", values[1]))));
    assertU(commit());

    assertQ(
        req("q", "id:1"),
        "//result/doc[1]/arr[@name='" + field + "']/" + type + "[.='" + values[0] + "']",
        "//result/doc[1]/arr[@name='" + field + "']/" + type + "[.='" + values[1] + "']",
        "count(//result/doc[1]/arr[@name='" + field + "']/" + type + ")=2");

    assertU(adoc(sdoc("id", "1", field, Map.of("remove", values[0]))));
    assertU(commit());

    assertQ(
        req("q", "id:1"),
        "//result/doc[1]/arr[@name='" + field + "']/" + type + "[.='" + values[1] + "']",
        "count(//result/doc[1]/arr[@name='" + field + "']/" + type + ")=1");

    assertU(adoc(sdoc("id", "1", field, Map.of("set", Arrays.asList(values)))));
    assertU(commit());

    assertQ(
        req("q", "id:1"),
        "//result/doc[1]/arr[@name='" + field + "']/" + type + "[.='" + values[0] + "']",
        "//result/doc[1]/arr[@name='" + field + "']/" + type + "[.='" + values[1] + "']",
        "//result/doc[1]/arr[@name='" + field + "']/" + type + "[.='" + values[2] + "']",
        "count(//result/doc[1]/arr[@name='" + field + "']/" + type + ")=3");

    assertU(adoc(sdoc("id", "1", field, Map.of("removeregex", ".*"))));
    assertU(commit());

    assertQ(req("q", "id:1"), "count(//result/doc[1]/arr[@name='" + field + "']/" + type + ")=0");
  }

  private void doTestIntPointFieldsAtomicUpdates(String field) {
    int number1 = random().nextInt();
    int number2;
    long inc1;
    for (; ; ) {
      number2 = random().nextInt();
      inc1 = (long) number2 - number1;
      if (Math.abs(inc1) < (long) Integer.MAX_VALUE) {
        break;
      }
    }
    assertU(adoc(sdoc("id", "1", field, String.valueOf(number1))));
    assertU(commit());

    assertU(adoc(sdoc("id", "1", field, Map.of("inc", (int) inc1))));
    assertU(commit());

    assertQ(req("q", "id:1"), "//result/doc[1]/int[@name='" + field + "'][.='" + number2 + "']");

    int number3 = random().nextInt();
    assertU(adoc(sdoc("id", "1", field, Map.of("set", number3))));
    assertU(commit());

    assertQ(req("q", "id:1"), "//result/doc[1]/int[@name='" + field + "'][.='" + number3 + "']");
  }

  private void doTestLongPointFieldsAtomicUpdates(String field) {
    long number1 = random().nextLong();
    long number2;
    BigInteger inc1;
    for (; ; ) {
      number2 = random().nextLong();
      inc1 = BigInteger.valueOf(number2).subtract(BigInteger.valueOf(number1));
      if (inc1.abs().compareTo(BigInteger.valueOf(Long.MAX_VALUE)) <= 0) {
        break;
      }
    }
    assertU(adoc(sdoc("id", "1", field, String.valueOf(number1))));
    assertU(commit());

    assertU(adoc(sdoc("id", "1", field, Map.of("inc", inc1.longValueExact()))));
    assertU(commit());

    assertQ(req("q", "id:1"), "//result/doc[1]/long[@name='" + field + "'][.='" + number2 + "']");

    long number3 = random().nextLong();
    assertU(adoc(sdoc("id", "1", field, Map.of("set", number3))));
    assertU(commit());

    assertQ(req("q", "id:1"), "//result/doc[1]/long[@name='" + field + "'][.='" + number3 + "']");
  }

  private void doTestFloatPointFieldExactQuery(final String field, boolean testDouble)
      throws Exception {
    doTestFloatPointFieldExactQuery(field, true, testDouble);
  }

  /**
   * @param field the field to use for indexing and searching against
   * @param searchable set to true if searches against "field" should succeed, false if field is
   *     only stored and searches should always get numFound=0
   */
  private void doTestFloatPointFieldExactQuery(
      final String field, final boolean searchable, final boolean testDouble) {
    int numValues = 10 * RANDOM_MULTIPLIER;
    Map<String, Integer> randCount = new HashMap<>(numValues);
    String[] rand =
        testDouble
            ? toStringArray(getRandomDoubles(numValues, false))
            : toStringArray(getRandomFloats(numValues, false));
    for (int i = 0; i < numValues; i++) {
      randCount.merge(rand[i], 1, (a, b) -> a + b); // count unique values
      assertU(adoc("id", String.valueOf(i), field, rand[i]));
    }
    assertU(commit());

    for (int i = 0; i < numValues; i++) {
      assertQ(
          req(
              "q",
              field + ":" + (rand[i].startsWith("-") ? "\\" : "") + rand[i],
              "fl",
              "id," + field),
          getTestString(searchable, randCount.get(rand[i])));
    }

    StringBuilder builder = new StringBuilder();
    for (String value : randCount.keySet()) {
      if (builder.length() != 0) {
        builder.append(" OR ");
      }
      if (value.startsWith("-")) {
        builder.append("\\"); // escape negative sign
      }
      builder.append(value);
    }
    assertQ(
        req("debug", "true", "q", field + ":(" + builder + ")"),
        getTestString(searchable, numValues));

    clearIndex();
    assertU(commit());
  }

  /**
   * For each value, creates a doc with that value in the specified field and then asserts that
   * asc/desc sorts on that field succeeds and that the docs are in the (relatively) expected order
   *
   * @param field name of field to sort on
   * @param values list of values in ascending order
   */
  private <T extends Comparable<T>> void doTestPointFieldSort(String field, List<T> values) {
    assertTrue(values != null && 2 <= values.size());

    final List<SolrInputDocument> docs = new ArrayList<>(values.size());
    final String[] ascXpathChecks = new String[values.size() + 1];
    final String[] descXpathChecks = new String[values.size() + 1];
    ascXpathChecks[values.size()] = "//*[@numFound='" + values.size() + "']";
    descXpathChecks[values.size()] = "//*[@numFound='" + values.size() + "']";

    boolean missingFirst = field.endsWith("_sml") == false;

    List<PosVal<T>> ascendingPosVals = toAscendingPosVals(values, missingFirst);
    for (int i = ascendingPosVals.size() - 1; i >= 0; --i) {
      T value = ascendingPosVals.get(i).val;
      if (value == null) {
        docs.add(sdoc("id", String.valueOf(i))); // null => missing value
      } else {
        docs.add(sdoc("id", String.valueOf(i), field, String.valueOf(value)));
      }
      // reminder: xpath array indexes start at 1
      ascXpathChecks[i] = "//result/doc[" + (1 + i) + "]/str[@name='id'][.='" + i + "']";
    }
    List<PosVal<T>> descendingPosVals =
        toDescendingPosVals(
            ascendingPosVals.stream().map(pv -> pv.val).collect(Collectors.toList()), missingFirst);
    for (int i = descendingPosVals.size() - 1; i >= 0; --i) {
      descXpathChecks[i] =
          "//result/doc[" + (i + 1) + "]/str[@name='id'][.='" + descendingPosVals.get(i).pos + "']";
    }

    // ensure doc add order doesn't affect results
    Collections.shuffle(docs, random());
    for (SolrInputDocument doc : docs) {
      assertU(adoc(doc));
    }
    assertU(commit());

    assertQ(req("q", "*:*", "fl", "id, " + field, "sort", field + " asc, id asc"), ascXpathChecks);
    assertQ(
        req("q", "*:*", "fl", "id, " + field, "sort", field + " desc, id desc"), descXpathChecks);

    clearIndex();
    assertU(commit());
  }

  /**
   * Checks that the specified field can not be sorted on, even if there are documents with (all)
   * the specified values in the index.
   *
   * @param field the field name to try and sort on
   * @param errSubStr substring to look for in the error msg
   * @param values one or more values to put into the doc(s) in the index - may be more than one for
   *     multivalued fields
   */
  private void doTestPointFieldSortError(String field, String errSubStr, String... values) {

    final int numDocs = atLeast(random(), 10);
    for (int i = 0; i < numDocs; i++) {
      SolrInputDocument doc = sdoc("id", String.valueOf(i));
      for (String v : values) {
        doc.addField(field, v);
      }
      assertU(adoc(doc));
    }

    assertQEx(
        "Should not be able to sort on field: " + field,
        errSubStr,
        req("q", "*:*", "fl", "id", "sort", field + " desc"),
        SolrException.ErrorCode.BAD_REQUEST);

    clearIndex();
    assertU(commit());

    // empty index should (also) give same error
    assertQEx(
        "Should not be able to sort on field: " + field,
        errSubStr,
        req("q", "*:*", "fl", "id", "sort", field + " desc"),
        SolrException.ErrorCode.BAD_REQUEST);
  }

  private void doTestFloatPointFieldRangeQuery(String fieldName, String type, boolean testDouble) {
    for (int i = 9; i >= 0; i--) {
      assertU(adoc("id", String.valueOf(i), fieldName, String.valueOf(i)));
    }
    assertU(commit());
    assertQ(
        req("q", fieldName + ":[0 TO 3]", "fl", "id, " + fieldName, "sort", "id asc"),
        "//*[@numFound='4']",
        "//result/doc[1]/" + type + "[@name='" + fieldName + "'][.='0.0']",
        "//result/doc[2]/" + type + "[@name='" + fieldName + "'][.='1.0']",
        "//result/doc[3]/" + type + "[@name='" + fieldName + "'][.='2.0']",
        "//result/doc[4]/" + type + "[@name='" + fieldName + "'][.='3.0']");

    assertQ(
        req("q", fieldName + ":{0 TO 3]", "fl", "id, " + fieldName, "sort", "id asc"),
        "//*[@numFound='3']",
        "//result/doc[1]/" + type + "[@name='" + fieldName + "'][.='1.0']",
        "//result/doc[2]/" + type + "[@name='" + fieldName + "'][.='2.0']",
        "//result/doc[3]/" + type + "[@name='" + fieldName + "'][.='3.0']");

    assertQ(
        req("q", fieldName + ":[0 TO 3}", "fl", "id, " + fieldName, "sort", "id asc"),
        "//*[@numFound='3']",
        "//result/doc[1]/" + type + "[@name='" + fieldName + "'][.='0.0']",
        "//result/doc[2]/" + type + "[@name='" + fieldName + "'][.='1.0']",
        "//result/doc[3]/" + type + "[@name='" + fieldName + "'][.='2.0']");

    assertQ(
        req("q", fieldName + ":{0 TO 3}", "fl", "id, " + fieldName, "sort", "id asc"),
        "//*[@numFound='2']",
        "//result/doc[1]/" + type + "[@name='" + fieldName + "'][.='1.0']",
        "//result/doc[2]/" + type + "[@name='" + fieldName + "'][.='2.0']");

    assertQ(
        req("q", fieldName + ":{0 TO *}", "fl", "id, " + fieldName, "sort", "id asc"),
        "//*[@numFound='9']",
        "//result/doc[1]/" + type + "[@name='" + fieldName + "'][.='1.0']");

    assertQ(
        req("q", fieldName + ":{* TO 3}", "fl", "id, " + fieldName, "sort", "id asc"),
        "//*[@numFound='3']",
        "//result/doc[1]/" + type + "[@name='" + fieldName + "'][.='0.0']",
        "//result/doc[2]/" + type + "[@name='" + fieldName + "'][.='1.0']",
        "//result/doc[3]/" + type + "[@name='" + fieldName + "'][.='2.0']");

    assertQ(
        req("q", fieldName + ":[* TO 3}", "fl", "id, " + fieldName, "sort", "id asc"),
        "//*[@numFound='3']",
        "//result/doc[1]/" + type + "[@name='" + fieldName + "'][.='0.0']",
        "//result/doc[2]/" + type + "[@name='" + fieldName + "'][.='1.0']",
        "//result/doc[3]/" + type + "[@name='" + fieldName + "'][.='2.0']");

    assertQ(
        req("q", fieldName + ":[* TO *}", "fl", "id, " + fieldName, "sort", "id asc"),
        "//*[@numFound='10']",
        "//result/doc[1]/" + type + "[@name='" + fieldName + "'][.='0.0']",
        "//result/doc[10]/" + type + "[@name='" + fieldName + "'][.='9.0']");

    assertQ(
        req("q", fieldName + ":[0.9 TO 1.01]", "fl", "id, " + fieldName),
        "//*[@numFound='1']",
        "//result/doc[1]/" + type + "[@name='" + fieldName + "'][.='1.0']");

    assertQ(
        req("q", fieldName + ":{0.9 TO 1.01}", "fl", "id, " + fieldName),
        "//*[@numFound='1']",
        "//result/doc[1]/" + type + "[@name='" + fieldName + "'][.='1.0']");

    clearIndex();
    assertU(commit());

    String[] arr;
    if (testDouble) {
      arr = toAscendingStringArray(getRandomDoubles(10, false), true);
    } else {
      arr = toAscendingStringArray(getRandomFloats(10, false), true);
    }
    for (int i = 0; i < arr.length; i++) {
      assertU(adoc("id", String.valueOf(i), fieldName, arr[i]));
    }
    assertU(commit());
    for (int i = 0; i < arr.length; i++) {
      assertQ(
          req("q", fieldName + ":[" + arr[0] + " TO " + arr[i] + "]", "fl", "id, " + fieldName),
          "//*[@numFound='" + (i + 1) + "']");
      assertQ(
          req("q", fieldName + ":{" + arr[0] + " TO " + arr[i] + "}", "fl", "id, " + fieldName),
          "//*[@numFound='" + (Math.max(0, i - 1)) + "']");
    }
    doTestDoubleFloatRangeLimits(fieldName, testDouble);
  }

  private void doTestDoubleFloatRangeLimits(String fieldName, boolean testDouble) {
    // POSITIVE/NEGATIVE_INFINITY toString is the same for Double and Float, it's OK to use this
    // code for both cases
    String positiveInfinity = String.valueOf(Double.POSITIVE_INFINITY);
    String negativeInfinity = String.valueOf(Double.NEGATIVE_INFINITY);
    String minVal = String.valueOf(testDouble ? Double.MIN_VALUE : Float.MIN_VALUE);
    String maxVal = String.valueOf(testDouble ? Double.MAX_VALUE : Float.MAX_VALUE);
    String negativeMinVal = "-" + minVal;
    String negativeMaxVal = "-" + maxVal;
    clearIndex();
    assertU(adoc("id", "1", fieldName, minVal));
    assertU(adoc("id", "2", fieldName, maxVal));
    assertU(adoc("id", "3", fieldName, negativeInfinity));
    assertU(adoc("id", "4", fieldName, positiveInfinity));
    assertU(adoc("id", "5", fieldName, negativeMinVal));
    assertU(adoc("id", "6", fieldName, negativeMaxVal));
    assertU(commit());
    // negative to negative
    assertAllInclusiveExclusiveVariations(fieldName, "*", "-1", 2, 2, 2, 2);
    assertAllInclusiveExclusiveVariations(fieldName, negativeInfinity, "-1", 1, 2, 1, 2);
    assertAllInclusiveExclusiveVariations(fieldName, negativeMaxVal, negativeMinVal, 0, 1, 1, 2);
    // negative to cero
    assertAllInclusiveExclusiveVariations(fieldName, "*", "-0.0f", 3, 3, 3, 3);
    assertAllInclusiveExclusiveVariations(fieldName, negativeInfinity, "-0.0f", 2, 3, 2, 3);
    assertAllInclusiveExclusiveVariations(fieldName, negativeMinVal, "-0.0f", 0, 1, 0, 1);

    assertAllInclusiveExclusiveVariations(fieldName, "*", "0", 3, 3, 3, 3);
    assertAllInclusiveExclusiveVariations(fieldName, negativeInfinity, "0", 2, 3, 2, 3);
    assertAllInclusiveExclusiveVariations(fieldName, negativeMinVal, "0", 0, 1, 0, 1);
    // negative to positive
    assertAllInclusiveExclusiveVariations(fieldName, "*", "1", 4, 4, 4, 4);
    assertAllInclusiveExclusiveVariations(fieldName, "-1", "*", 4, 4, 4, 4);
    assertAllInclusiveExclusiveVariations(fieldName, "-1", "1", 2, 2, 2, 2);
    assertAllInclusiveExclusiveVariations(fieldName, "*", "*", 6, 6, 6, 6);

    assertAllInclusiveExclusiveVariations(fieldName, "-1", positiveInfinity, 3, 3, 4, 4);
    assertAllInclusiveExclusiveVariations(fieldName, negativeInfinity, "1", 3, 4, 3, 4);
    assertAllInclusiveExclusiveVariations(
        fieldName, negativeInfinity, positiveInfinity, 4, 5, 5, 6);

    assertAllInclusiveExclusiveVariations(fieldName, negativeMinVal, minVal, 0, 1, 1, 2);
    assertAllInclusiveExclusiveVariations(fieldName, negativeMaxVal, maxVal, 2, 3, 3, 4);
    // cero to positive
    assertAllInclusiveExclusiveVariations(fieldName, "-0.0f", "*", 3, 3, 3, 3);
    assertAllInclusiveExclusiveVariations(fieldName, "-0.0f", positiveInfinity, 2, 2, 3, 3);
    assertAllInclusiveExclusiveVariations(fieldName, "-0.0f", minVal, 0, 0, 1, 1);

    assertAllInclusiveExclusiveVariations(fieldName, "0", "*", 3, 3, 3, 3);
    assertAllInclusiveExclusiveVariations(fieldName, "0", positiveInfinity, 2, 2, 3, 3);
    assertAllInclusiveExclusiveVariations(fieldName, "0", minVal, 0, 0, 1, 1);
    // positive to positive
    assertAllInclusiveExclusiveVariations(fieldName, "1", "*", 2, 2, 2, 2);
    assertAllInclusiveExclusiveVariations(fieldName, "1", positiveInfinity, 1, 1, 2, 2);
    assertAllInclusiveExclusiveVariations(fieldName, minVal, maxVal, 0, 1, 1, 2);

    // inverted limits
    assertAllInclusiveExclusiveVariations(fieldName, "1", "-1", 0, 0, 0, 0);
    assertAllInclusiveExclusiveVariations(
        fieldName, positiveInfinity, negativeInfinity, 0, 0, 0, 0);
    assertAllInclusiveExclusiveVariations(fieldName, minVal, negativeMinVal, 0, 0, 0, 0);

    // MatchNoDocs cases
    assertAllInclusiveExclusiveVariations(
        fieldName, negativeInfinity, negativeInfinity, 0, 0, 0, 1);
    assertAllInclusiveExclusiveVariations(
        fieldName, positiveInfinity, positiveInfinity, 0, 0, 0, 1);

    clearIndex();
    assertU(adoc("id", "1", fieldName, "0.0"));
    assertU(adoc("id", "2", fieldName, "-0.0"));
    assertU(commit());
    assertAllInclusiveExclusiveVariations(fieldName, "*", "*", 2, 2, 2, 2);
    assertAllInclusiveExclusiveVariations(fieldName, "*", "0", 1, 1, 2, 2);
    assertAllInclusiveExclusiveVariations(fieldName, "0", "*", 0, 1, 0, 1);
    assertAllInclusiveExclusiveVariations(fieldName, "*", "-0.0f", 0, 0, 1, 1);
    assertAllInclusiveExclusiveVariations(fieldName, "-0.0f", "*", 1, 2, 1, 2);
    assertAllInclusiveExclusiveVariations(fieldName, "-0.0f", "0", 0, 1, 1, 2);
  }

  private void assertAllInclusiveExclusiveVariations(
      String fieldName,
      String min,
      String max,
      int countExclusiveExclusive,
      int countInclusiveExclusive,
      int countExclusiveInclusive,
      int countInclusiveInclusive) {
    assertQ(
        req("q", fieldName + ":{" + min + " TO " + max + "}", "fl", "id, " + fieldName),
        "//*[@numFound='" + countExclusiveExclusive + "']");
    assertQ(
        req("q", fieldName + ":[" + min + " TO " + max + "}", "fl", "id, " + fieldName),
        "//*[@numFound='" + countInclusiveExclusive + "']");
    assertQ(
        req("q", fieldName + ":{" + min + " TO " + max + "]", "fl", "id, " + fieldName),
        "//*[@numFound='" + countExclusiveInclusive + "']");
    assertQ(
        req("q", fieldName + ":[" + min + " TO " + max + "]", "fl", "id, " + fieldName),
        "//*[@numFound='" + countInclusiveInclusive + "']");
  }

  private void doTestFloatPointFunctionQuery(String field) {
    assertTrue(h.getCore().getLatestSchema().getField(field).getType() instanceof PointField);
    int numVals = 10 * RANDOM_MULTIPLIER;
    List<Float> values = getRandomFloats(numVals, false);
    String assertNumFound = "//*[@numFound='" + numVals + "']";
    String[] idAscXpathChecks = new String[numVals + 1];
    String[] idAscNegXpathChecks = new String[numVals + 1];
    idAscXpathChecks[0] = assertNumFound;
    idAscNegXpathChecks[0] = assertNumFound;
    for (int i = 0; i < values.size(); ++i) {
      assertU(
          adoc(
              "id",
              Character.valueOf((char) ('A' + i)).toString(),
              field,
              String.valueOf(values.get(i))));
      // reminder: xpath array indexes start at 1
      idAscXpathChecks[i + 1] =
          "//result/doc["
              + (1 + i)
              + "]/float[@name='field("
              + field
              + ")'][.='"
              + values.get(i)
              + "']";
      idAscNegXpathChecks[i + 1] =
          "//result/doc["
              + (1 + i)
              + "]/float[@name='product(-1,"
              + field
              + ")'][.='"
              + (-1.0f * values.get(i))
              + "']";
    }
    assertU(commit());
    assertQ(
        req(
            "q",
            "*:*",
            "fl",
            "id, " + field + ", field(" + field + ")",
            "rows",
            String.valueOf(numVals),
            "sort",
            "id asc"),
        idAscXpathChecks);
    assertQ(
        req(
            "q",
            "*:*",
            "fl",
            "id, " + field + ", product(-1," + field + ")",
            "rows",
            String.valueOf(numVals),
            "sort",
            "id asc"),
        idAscNegXpathChecks);

    List<PosVal<Float>> ascNegPosVals =
        toAscendingPosVals(values.stream().map(v -> -v).collect(Collectors.toList()), true);
    String[] ascNegXpathChecks = new String[numVals + 1];
    ascNegXpathChecks[0] = assertNumFound;
    for (int i = 0; i < ascNegPosVals.size(); ++i) {
      PosVal<Float> posVal = ascNegPosVals.get(i);
      ascNegXpathChecks[i + 1] =
          "//result/doc["
              + (1 + i)
              + "]/float[@name='"
              + field
              + "'][.='"
              + values.get(posVal.pos)
              + "']";
    }
    assertQ(
        req(
            "q",
            "*:*",
            "fl",
            "id, " + field,
            "rows",
            String.valueOf(numVals),
            "sort",
            "product(-1," + field + ") asc"),
        ascNegXpathChecks);

    clearIndex();
    assertU(commit());
  }

  private void doTestDoublePointFunctionQuery(String field) {
    assertTrue(h.getCore().getLatestSchema().getField(field).getType() instanceof PointField);
    int numVals = 10 * RANDOM_MULTIPLIER;
    // Restrict values to float range; otherwise conversion to float will cause truncation ->
    // undefined results
    List<Double> values =
        getRandomList(
            numVals,
            false,
            () -> {
              Float f = Float.NaN;
              while (f.isNaN()) {
                f = Float.intBitsToFloat(random().nextInt());
              }
              return f.doubleValue();
            });
    String assertNumFound = "//*[@numFound='" + numVals + "']";
    String[] idAscXpathChecks = new String[numVals + 1];
    String[] idAscNegXpathChecks = new String[numVals + 1];
    idAscXpathChecks[0] = assertNumFound;
    idAscNegXpathChecks[0] = assertNumFound;
    for (int i = 0; i < values.size(); ++i) {
      assertU(
          adoc(
              "id",
              Character.valueOf((char) ('A' + i)).toString(),
              field,
              String.valueOf(values.get(i))));
      // reminder: xpath array indexes start at 1
      idAscXpathChecks[i + 1] =
          "//result/doc["
              + (1 + i)
              + "]/double[@name='field("
              + field
              + ")'][.='"
              + values.get(i)
              + "']";
      idAscNegXpathChecks[i + 1] =
          "//result/doc["
              + (1 + i)
              + "]/float[@name='product(-1,"
              + field
              + ")'][.='"
              + (-1.0f * values.get(i).floatValue())
              + "']";
    }
    assertU(commit());
    assertQ(
        req(
            "q",
            "*:*",
            "fl",
            "id, " + field + ", field(" + field + ")",
            "rows",
            String.valueOf(numVals),
            "sort",
            "id asc"),
        idAscXpathChecks);
    assertQ(
        req(
            "q",
            "*:*",
            "fl",
            "id, " + field + ", product(-1," + field + ")",
            "rows",
            String.valueOf(numVals),
            "sort",
            "id asc"),
        idAscNegXpathChecks);

    // Intentionally use floats here to mimic server-side function sorting
    List<PosVal<Float>> ascNegPosVals =
        toAscendingPosVals(
            values.stream().map(v -> -v.floatValue()).collect(Collectors.toList()), true);
    String[] ascNegXpathChecks = new String[numVals + 1];
    ascNegXpathChecks[0] = assertNumFound;
    for (int i = 0; i < ascNegPosVals.size(); ++i) {
      PosVal<Float> posVal = ascNegPosVals.get(i);
      ascNegXpathChecks[i + 1] =
          "//result/doc["
              + (1 + i)
              + "]/double[@name='"
              + field
              + "'][.='"
              + values.get(posVal.pos)
              + "']";
    }
    assertQ(
        req(
            "q",
            "*:*",
            "fl",
            "id, " + field,
            "rows",
            String.valueOf(numVals),
            "sort",
            "product(-1," + field + ") asc"),
        ascNegXpathChecks);

    clearIndex();
    assertU(commit());
  }

  private void doTestSetQueries(String fieldName, String[] values, boolean multiValued) {
    for (int i = 0; i < values.length; i++) {
      assertU(adoc("id", String.valueOf(i), fieldName, values[i]));
    }
    assertU(commit());
    SchemaField sf = h.getCore().getLatestSchema().getField(fieldName);
    assertTrue(sf.getType() instanceof PointField);

    for (String value : values) {
      assertQ(
          req("q", "{!term f='" + fieldName + "'}" + value, "fl", "id," + fieldName),
          "//*[@numFound='1']");
    }

    for (int i = 0; i < values.length; i++) {
      assertQ(
          req(
              "q",
              "{!terms f='" + fieldName + "'}" + values[i] + "," + values[(i + 1) % values.length],
              "fl",
              "id," + fieldName),
          "//*[@numFound='2']");
    }

    assertTrue(values.length > SolrQueryParser.TERMS_QUERY_THRESHOLD);
    int numTerms = SolrQueryParser.TERMS_QUERY_THRESHOLD + 1;
    StringBuilder builder = new StringBuilder(fieldName + ":(");
    for (int i = 0; i < numTerms; i++) {
      if (sf.getType().getNumberType() == NumberType.DATE) {
        builder.append(values[i].replaceAll("(:|^[-+])", "\\\\$1")).append(' ');
      } else {
        builder.append(String.valueOf(values[i]).replace("-", "\\-")).append(' ');
      }
    }
    builder.append(')');
    if (sf.indexed()) { // SolrQueryParser should also be generating a PointInSetQuery if indexed
      assertQ(
          req(
              CommonParams.DEBUG,
              CommonParams.QUERY,
              "q",
              "*:*",
              "fq",
              builder.toString(),
              "fl",
              "id," + fieldName),
          "//*[@numFound='" + numTerms + "']",
          "//*[@name='parsed_filter_queries']/str[.='"
              + getSetQueryToString(fieldName, values, numTerms)
              + "']");
    } else {
      // Won't use PointInSetQuery if the field is not indexed, but should match the same docs
      assertQ(
          req(
              CommonParams.DEBUG,
              CommonParams.QUERY,
              "q",
              "*:*",
              "fq",
              builder.toString(),
              "fl",
              "id," + fieldName),
          "//*[@numFound='" + numTerms + "']");
    }

    if (multiValued) {
      clearIndex();
      assertU(commit());
      for (int i = 0; i < values.length; i++) {
        assertU(
            adoc(
                "id",
                String.valueOf(i),
                fieldName,
                values[i],
                fieldName,
                values[(i + 1) % values.length]));
      }
      assertU(commit());
      for (String value : values) {
        assertQ(
            req("q", "{!term f='" + fieldName + "'}" + value, "fl", "id," + fieldName),
            "//*[@numFound='2']");
      }

      for (int i = 0; i < values.length; i++) {
        assertQ(
            req(
                "q",
                "{!terms f='"
                    + fieldName
                    + "'}"
                    + values[i]
                    + ","
                    + values[(i + 1) % values.length],
                "fl",
                "id," + fieldName),
            "//*[@numFound='3']");
      }
    }
  }

  private String getSetQueryToString(String fieldName, String[] values, int numTerms) {
    SchemaField sf = h.getCore().getLatestSchema().getField(fieldName);
    Query setQuery =
        sf.getType().getSetQuery(null, sf, Arrays.asList(Arrays.copyOf(values, numTerms)));
    if (sf.indexed() && sf.hasDocValues()) {
      return IndexOrDocValuesQuery.class.getSimpleName() + "(" + setQuery.toString() + ")";
    }
    return "(" + setQuery.toString() + ")";
  }

  private void doTestDatePointFieldExactQuery(final String field, final String baseDate)
      throws Exception {
    doTestDatePointFieldExactQuery(field, baseDate, true);
  }

  /**
   * @param field the field to use for indexing and searching against
   * @param baseDate basic value to use for indexing and searching
   * @param searchable set to true if searches against "field" should succeed, false if field is
   *     only stored and searches should always get numFound=0
   */
  private void doTestDatePointFieldExactQuery(
      final String field, final String baseDate, final boolean searchable) {
    final String MATCH_ONE = "//*[@numFound='" + (searchable ? "1" : "0") + "']";
    final String MATCH_TWO = "//*[@numFound='" + (searchable ? "2" : "0") + "']";

    for (int i = 0; i < 10; i++) {
      assertU(
          adoc(
              "id",
              String.valueOf(i),
              field,
              String.format(Locale.ROOT, "%s+%dMINUTES", baseDate, i + 1)));
    }
    assertU(commit());
    for (int i = 0; i < 10; i++) {
      String date = String.format(Locale.ROOT, "%s+%dMINUTES", baseDate, i + 1);
      assertQ(req("q", field + ":\"" + date + "\"", "fl", "id, " + field), MATCH_ONE);
    }

    for (int i = 0; i < 10; i++) {
      String date1 = String.format(Locale.ROOT, "%s+%dMINUTES", baseDate, i + 1);
      String date2 = String.format(Locale.ROOT, "%s+%dMINUTES", baseDate, ((i + 1) % 10 + 1));
      assertQ(
          req("q", field + ":\"" + date1 + "\"" + " OR " + field + ":\"" + date2 + "\""),
          MATCH_TWO);
    }

    clearIndex();
    assertU(commit());
  }

  private void doTestDatePointFieldRangeQuery(String fieldName) {
    String baseDate = "1995-12-31T10:59:59Z";
    for (int i = 9; i >= 0; i--) {
      assertU(
          adoc(
              "id",
              String.valueOf(i),
              fieldName,
              String.format(Locale.ROOT, "%s+%dHOURS", baseDate, i)));
    }
    assertU(commit());
    assertQ(
        req(
            "q",
            fieldName
                + ":"
                + String.format(Locale.ROOT, "[%s+0HOURS TO %s+3HOURS]", baseDate, baseDate),
            "fl",
            "id, " + fieldName,
            "sort",
            "id asc"),
        "//*[@numFound='4']",
        "//result/doc[1]/date[@name='" + fieldName + "'][.='1995-12-31T10:59:59Z']",
        "//result/doc[2]/date[@name='" + fieldName + "'][.='1995-12-31T11:59:59Z']",
        "//result/doc[3]/date[@name='" + fieldName + "'][.='1995-12-31T12:59:59Z']",
        "//result/doc[4]/date[@name='" + fieldName + "'][.='1995-12-31T13:59:59Z']");

    assertQ(
        req(
            "q",
            fieldName
                + ":"
                + String.format(Locale.ROOT, "{%s+0HOURS TO %s+3HOURS]", baseDate, baseDate),
            "fl",
            "id, " + fieldName,
            "sort",
            "id asc"),
        "//*[@numFound='3']",
        "//result/doc[1]/date[@name='" + fieldName + "'][.='1995-12-31T11:59:59Z']",
        "//result/doc[2]/date[@name='" + fieldName + "'][.='1995-12-31T12:59:59Z']",
        "//result/doc[3]/date[@name='" + fieldName + "'][.='1995-12-31T13:59:59Z']");

    assertQ(
        req(
            "q",
            fieldName
                + ":"
                + String.format(Locale.ROOT, "[%s+0HOURS TO %s+3HOURS}", baseDate, baseDate),
            "fl",
            "id, " + fieldName,
            "sort",
            "id asc"),
        "//*[@numFound='3']",
        "//result/doc[1]/date[@name='" + fieldName + "'][.='1995-12-31T10:59:59Z']",
        "//result/doc[2]/date[@name='" + fieldName + "'][.='1995-12-31T11:59:59Z']",
        "//result/doc[3]/date[@name='" + fieldName + "'][.='1995-12-31T12:59:59Z']");

    assertQ(
        req(
            "q",
            fieldName
                + ":"
                + String.format(Locale.ROOT, "{%s+0HOURS TO %s+3HOURS}", baseDate, baseDate),
            "fl",
            "id, " + fieldName,
            "sort",
            "id asc"),
        "//*[@numFound='2']",
        "//result/doc[1]/date[@name='" + fieldName + "'][.='1995-12-31T11:59:59Z']",
        "//result/doc[2]/date[@name='" + fieldName + "'][.='1995-12-31T12:59:59Z']");

    assertQ(
        req(
            "q",
            fieldName + ":" + String.format(Locale.ROOT, "{%s+0HOURS TO *}", baseDate),
            "fl",
            "id, " + fieldName,
            "sort",
            "id asc"),
        "//*[@numFound='9']",
        "//result/doc[1]/date[@name='" + fieldName + "'][.='1995-12-31T11:59:59Z']");

    assertQ(
        req(
            "q",
            fieldName + ":" + String.format(Locale.ROOT, "{* TO %s+3HOURS}", baseDate),
            "fl",
            "id, " + fieldName,
            "sort",
            "id asc"),
        "//*[@numFound='3']",
        "//result/doc[1]/date[@name='" + fieldName + "'][.='1995-12-31T10:59:59Z']");

    assertQ(
        req(
            "q",
            fieldName + ":" + String.format(Locale.ROOT, "[* TO %s+3HOURS}", baseDate),
            "fl",
            "id, " + fieldName,
            "sort",
            "id asc"),
        "//*[@numFound='3']",
        "//result/doc[1]/date[@name='" + fieldName + "'][.='1995-12-31T10:59:59Z']");

    assertQ(
        req("q", fieldName + ":[* TO *}", "fl", "id, " + fieldName, "sort", "id asc"),
        "//*[@numFound='10']",
        "//result/doc[1]/date[@name='" + fieldName + "'][.='1995-12-31T10:59:59Z']",
        "//result/doc[10]/date[@name='" + fieldName + "'][.='1995-12-31T19:59:59Z']");

    assertQ(
        req(
            "q",
            fieldName
                + ":"
                + String.format(Locale.ROOT, "[%s+0HOURS TO %s+1HOURS]", baseDate, baseDate)
                + " OR "
                + fieldName
                + ":"
                + String.format(Locale.ROOT, "[%s+8HOURS TO %s+9HOURS]", baseDate, baseDate),
            "fl",
            "id, " + fieldName,
            "sort",
            "id asc"),
        "//*[@numFound='4']",
        "//result/doc[1]/date[@name='" + fieldName + "'][.='1995-12-31T10:59:59Z']",
        "//result/doc[2]/date[@name='" + fieldName + "'][.='1995-12-31T11:59:59Z']",
        "//result/doc[3]/date[@name='" + fieldName + "'][.='1995-12-31T18:59:59Z']",
        "//result/doc[4]/date[@name='" + fieldName + "'][.='1995-12-31T19:59:59Z']");

    assertQ(
        req(
            "q",
            fieldName
                + ":"
                + String.format(Locale.ROOT, "[%s+0HOURS TO %s+1HOURS]", baseDate, baseDate)
                + " AND "
                + fieldName
                + ":"
                + String.format(Locale.ROOT, "[%s+1HOURS TO %s+2HOURS]", baseDate, baseDate),
            "fl",
            "id, " + fieldName),
        "//*[@numFound='1']",
        "//result/doc[1]/date[@name='" + fieldName + "'][.='1995-12-31T11:59:59Z']");

    assertQ(
        req(
            "q",
            fieldName
                + ":"
                + String.format(Locale.ROOT, "[%s+0HOURS TO %s+1HOURS]", baseDate, baseDate)
                + " AND NOT "
                + fieldName
                + ":"
                + String.format(Locale.ROOT, "[%s+1HOURS TO %s+2HOURS]", baseDate, baseDate),
            "fl",
            "id, " + fieldName),
        "//*[@numFound='1']",
        "//result/doc[1]/date[@name='" + fieldName + "'][.='1995-12-31T10:59:59Z']");

    clearIndex();
    assertU(commit());

    String[] arr = toAscendingStringArray(getRandomInstants(100, false), true);
    for (int i = 0; i < arr.length; ++i) {
      assertU(adoc("id", String.valueOf(i), fieldName, arr[i]));
    }
    assertU(commit());
    for (int i = 0; i < arr.length; ++i) {
      assertQ(
          req("q", fieldName + ":[" + arr[0] + " TO " + arr[i] + "]", "fl", "id," + fieldName),
          "//*[@numFound='" + (i + 1) + "']");
      assertQ(
          req("q", fieldName + ":{" + arr[0] + " TO " + arr[i] + "}", "fl", "id, " + fieldName),
          "//*[@numFound='" + (Math.max(0, i - 1)) + "']");
      assertQ(
          req(
              "q",
              fieldName + ":[" + arr[0] + " TO " + arr[i] + "] AND " + fieldName + ":\"" + arr[0]
                  + "\"",
              "fl",
              "id, " + fieldName),
          "//*[@numFound='1']");
    }
  }

  private void doTestDatePointFunctionQuery(String field) {
    // This method is intentionally not randomized, because sorting by function happens
    // at float precision, which causes ms(date) to give the same value for different dates.
    // See https://issues.apache.org/jira/browse/SOLR-11825

    final String baseDate = "1995-01-10T10:59:10Z";

    for (int i = 9; i >= 0; i--) {
      String date = String.format(Locale.ROOT, "%s+%dSECONDS", baseDate, i + 1);
      assertU(adoc("id", String.valueOf(i), field, date));
    }
    assertU(commit());
    assertTrue(h.getCore().getLatestSchema().getField(field).getType() instanceof DatePointField);
    assertQ(
        req(
            "q",
            "*:*",
            "fl",
            "id, " + field,
            "sort",
            "product(-1,ms(" + field + "," + baseDate + ")) asc"),
        "//*[@numFound='10']",
        "//result/doc[1]/date[@name='" + field + "'][.='1995-01-10T10:59:20Z']",
        "//result/doc[2]/date[@name='" + field + "'][.='1995-01-10T10:59:19Z']",
        "//result/doc[3]/date[@name='" + field + "'][.='1995-01-10T10:59:18Z']",
        "//result/doc[10]/date[@name='" + field + "'][.='1995-01-10T10:59:11Z']");

    assertQ(
        req(
            "q",
            "*:*",
            "fl",
            "id, " + field + ", ms(" + field + "," + baseDate + ")",
            "sort",
            "id asc"),
        "//*[@numFound='10']",
        "//result/doc[1]/float[@name='ms(" + field + "," + baseDate + ")'][.='1000.0']",
        "//result/doc[2]/float[@name='ms(" + field + "," + baseDate + ")'][.='2000.0']",
        "//result/doc[3]/float[@name='ms(" + field + "," + baseDate + ")'][.='3000.0']",
        "//result/doc[10]/float[@name='ms(" + field + "," + baseDate + ")'][.='10000.0']");

    assertQ(
        req("q", "*:*", "fl", "id, " + field + ", field(" + field + ")", "sort", "id asc"),
        "//*[@numFound='10']",
        "//result/doc[1]/date[@name='field(" + field + ")'][.='1995-01-10T10:59:11Z']",
        "//result/doc[2]/date[@name='field(" + field + ")'][.='1995-01-10T10:59:12Z']",
        "//result/doc[3]/date[@name='field(" + field + ")'][.='1995-01-10T10:59:13Z']",
        "//result/doc[10]/date[@name='field(" + field + ")'][.='1995-01-10T10:59:20Z']");
  }

  private void doTestDatePointStats(String field, String dvField, String[] dates) {
    for (int i = 0; i < dates.length; i++) {
      assertU(adoc("id", String.valueOf(i), dvField, dates[i], field, dates[i]));
    }
    assertU(adoc("id", String.valueOf(dates.length)));
    assertU(commit());
    assertTrue(h.getCore().getLatestSchema().getField(dvField).hasDocValues());
    assertTrue(h.getCore().getLatestSchema().getField(dvField).getType() instanceof PointField);
    assertQ(
        req("q", "*:*", "fl", "id, " + dvField, "stats", "true", "stats.field", dvField),
        "//*[@numFound='" + (dates.length + 1) + "']",
        "//lst[@name='stats']/lst[@name='stats_fields']/lst[@name='"
            + dvField
            + "']/date[@name='min'][.='"
            + dates[0]
            + "']",
        "//lst[@name='stats']/lst[@name='stats_fields']/lst[@name='"
            + dvField
            + "']/date[@name='max'][.='"
            + dates[dates.length - 1]
            + "']",
        "//lst[@name='stats']/lst[@name='stats_fields']/lst[@name='"
            + dvField
            + "']/long[@name='count'][.='"
            + dates.length
            + "']",
        "//lst[@name='stats']/lst[@name='stats_fields']/lst[@name='"
            + dvField
            + "']/long[@name='missing'][.='1']");

    assertFalse(h.getCore().getLatestSchema().getField(field).hasDocValues());
    assertTrue(h.getCore().getLatestSchema().getField(field).getType() instanceof PointField);
    assertQEx(
        "Expecting Exception",
        "Can't calculate stats on a PointField without docValues",
        req("q", "*:*", "fl", "id, " + field, "stats", "true", "stats.field", field),
        SolrException.ErrorCode.BAD_REQUEST);
  }

  private void doTestDatePointFieldsAtomicUpdates(String field) {
    long millis1 = random().nextLong() % MAX_DATE_EPOCH_MILLIS;
    long millis2;
    DateGapCeiling gap;
    for (; ; ) {
      millis2 = random().nextLong() % MAX_DATE_EPOCH_MILLIS;
      gap = new DateGapCeiling(millis2 - millis1);
      millis2 = gap.addTo(millis1); // adjust millis2 to the closest +/-UNIT gap
      break;
    }
    String date1 = Instant.ofEpochMilli(millis1).toString();
    String date2 = Instant.ofEpochMilli(millis2).toString();
    assertU(adoc(sdoc("id", "1", field, date1)));
    assertU(commit());

    assertQ(req("q", "id:1"), "//result/doc[1]/date[@name='" + field + "'][.='" + date1 + "']");

    assertU(adoc(sdoc("id", "1", field, Map.of("set", date1 + gap))));
    assertU(commit());

    assertQ(req("q", "id:1"), "//result/doc[1]/date[@name='" + field + "'][.='" + date2 + "']");
  }

  private void doTestInternals(String field, String[] values) throws IOException {
    assertTrue(h.getCore().getLatestSchema().getField(field).getType() instanceof PointField);
    for (int i = 0; i < 10; i++) {
      assertU(adoc("id", String.valueOf(i), field, values[i]));
    }
    assertU(commit());

    SchemaField sf = h.getCore().getLatestSchema().getField(field);
    boolean ignoredField = !(sf.indexed() || sf.stored() || sf.hasDocValues());
    h.getCore()
        .withSearcher(
            searcher -> {
              DirectoryReader ir = searcher.getIndexReader();
              // our own SlowCompositeReader to check DocValues on disk w/o the UninvertingReader
              // added by SolrIndexSearcher
              final LeafReader leafReaderForCheckingDVs =
                  SlowCompositeReaderWrapper.wrap(searcher.getRawReader());

              if (sf.indexed()) {
                assertEquals(
                    "Field " + field + " should have point values",
                    10,
                    PointValues.size(ir, field));
              } else {
                assertEquals(
                    "Field " + field + " should have no point values",
                    0,
                    PointValues.size(ir, field));
              }
              if (ignoredField) {
                assertEquals(
                    "Field " + field + " should not have docValues",
                    DocIdSetIterator.NO_MORE_DOCS,
                    DocValues.getSortedNumeric(leafReaderForCheckingDVs, field).nextDoc());
                assertEquals(
                    "Field " + field + " should not have docValues",
                    DocIdSetIterator.NO_MORE_DOCS,
                    DocValues.getNumeric(leafReaderForCheckingDVs, field).nextDoc());
                assertEquals(
                    "Field " + field + " should not have docValues",
                    DocIdSetIterator.NO_MORE_DOCS,
                    DocValues.getSorted(leafReaderForCheckingDVs, field).nextDoc());
                assertEquals(
                    "Field " + field + " should not have docValues",
                    DocIdSetIterator.NO_MORE_DOCS,
                    DocValues.getBinary(leafReaderForCheckingDVs, field).nextDoc());
              } else {
                if (sf.hasDocValues()) {
                  if (sf.multiValued()) {
                    assertNotEquals(
                        "Field " + field + " should have docValues",
                        DocIdSetIterator.NO_MORE_DOCS,
                        DocValues.getSortedNumeric(leafReaderForCheckingDVs, field).nextDoc());
                  } else {
                    assertNotEquals(
                        "Field " + field + " should have docValues",
                        DocIdSetIterator.NO_MORE_DOCS,
                        DocValues.getNumeric(leafReaderForCheckingDVs, field).nextDoc());
                  }
                } else {
                  expectThrows(
                      IllegalStateException.class,
                      () -> DocValues.getSortedNumeric(leafReaderForCheckingDVs, field));
                  expectThrows(
                      IllegalStateException.class,
                      () -> DocValues.getNumeric(leafReaderForCheckingDVs, field));
                }
                expectThrows(
                    IllegalStateException.class,
                    () -> DocValues.getSorted(leafReaderForCheckingDVs, field));
                expectThrows(
                    IllegalStateException.class,
                    () -> DocValues.getBinary(leafReaderForCheckingDVs, field));
              }
              for (LeafReaderContext leave : ir.leaves()) {
                LeafReader reader = leave.reader();
                StoredFields storedFields = reader.storedFields();
                for (int i = 0; i < reader.numDocs(); i++) {
                  Document doc = storedFields.document(i);
                  if (sf.stored()) {
                    assertNotNull("Field " + field + " not found. Doc: " + doc, doc.get(field));
                  } else {
                    assertNull(doc.get(field));
                  }
                }
              }
              return null;
            });
    clearIndex();
    assertU(commit());
  }

  public void testNonReturnable() throws Exception {
    String[] ints = toStringArray(getRandomInts(2, false));
    doTestReturnNonStored("foo_p_i_ni_ns", false, ints[0]);
    doTestReturnNonStored("foo_p_i_ni_dv_ns", true, ints[0]);
    doTestReturnNonStored("foo_p_i_ni_ns_mv", false, ints);
    doTestReturnNonStored("foo_p_i_ni_dv_ns_mv", true, ints);

    String[] longs = toStringArray(getRandomLongs(2, false));
    doTestReturnNonStored("foo_p_l_ni_ns", false, longs[0]);
    doTestReturnNonStored("foo_p_l_ni_dv_ns", true, longs[0]);
    doTestReturnNonStored("foo_p_l_ni_ns_mv", false, longs);
    doTestReturnNonStored("foo_p_l_ni_dv_ns_mv", true, longs);

    String[] floats = toStringArray(getRandomFloats(2, false));
    doTestReturnNonStored("foo_p_f_ni_ns", false, floats[0]);
    doTestReturnNonStored("foo_p_f_ni_dv_ns", true, floats[0]);
    doTestReturnNonStored("foo_p_f_ni_ns_mv", false, floats);
    doTestReturnNonStored("foo_p_f_ni_dv_ns_mv", true, floats);

    String[] doubles = toStringArray(getRandomDoubles(2, false));
    doTestReturnNonStored("foo_p_d_ni_ns", false, doubles[0]);
    doTestReturnNonStored("foo_p_d_ni_dv_ns", true, doubles[0]);
    doTestReturnNonStored("foo_p_d_ni_ns_mv", false, doubles);
    doTestReturnNonStored("foo_p_d_ni_dv_ns_mv", true, doubles);

    String[] dates = new String[] {getRandomDateMaybeWithMath(), getRandomDateMaybeWithMath()};
    doTestReturnNonStored("foo_p_dt_ni_ns", false, dates[0]);
    doTestReturnNonStored("foo_p_dt_ni_dv_ns", true, dates[0]);
    doTestReturnNonStored("foo_p_dt_ni_ns_mv", false, dates);
    doTestReturnNonStored("foo_p_dt_ni_dv_ns_mv", true, dates);
  }

  public void doTestReturnNonStored(
      final String fieldName, boolean shouldReturnFieldIfRequested, final String... values) {
    final String RETURN_FIELD = "count(//doc/*[@name='" + fieldName + "'])=10";
    final String DONT_RETURN_FIELD = "count(//doc/*[@name='" + fieldName + "'])=0";
    assertFalse(h.getCore().getLatestSchema().getField(fieldName).stored());
    for (int i = 0; i < 10; i++) {
      SolrInputDocument doc = sdoc("id", String.valueOf(i));
      for (String value : values) {
        doc.addField(fieldName, value);
      }
      assertU(adoc(doc));
    }
    assertU(commit());
    assertQ(
        req("q", "*:*", "rows", "100", "fl", "id," + fieldName),
        "//*[@numFound='10']",
        "count(//doc)=10", // exactly 10 docs in response
        (shouldReturnFieldIfRequested
            ? RETURN_FIELD
            : DONT_RETURN_FIELD)); // no field in any doc other than 'id'

    assertQ(
        req("q", "*:*", "rows", "100", "fl", "*"),
        "//*[@numFound='10']",
        "count(//doc)=10", // exactly 10 docs in response
        DONT_RETURN_FIELD); // no field in any doc other than 'id'

    assertQ(
        req("q", "*:*", "rows", "100"),
        "//*[@numFound='10']",
        "count(//doc)=10", // exactly 10 docs in response
        DONT_RETURN_FIELD); // no field in any doc other than 'id'
    clearIndex();
    assertU(commit());
  }

  public void testWhiteboxCreateFields() throws Exception {
    String[] typeNames = new String[] {"i", "l", "f", "d", "dt"};
    Class<?>[] expectedClasses =
        new Class<?>[] {
          IntPoint.class, LongPoint.class, FloatPoint.class, DoublePoint.class, LongPoint.class
        };

    Date dateToTest = new Date();
    Object[][] values =
        new Object[][] {
          {42, "42"},
          {42, "42"},
          {42.123, "42.123"},
          {12345.6789, "12345.6789"},
          {
            dateToTest,
            new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ROOT).format(dateToTest),
            "NOW"
          } // "NOW" won't be equal to the other dates
        };

    Set<String> typesTested = new HashSet<>();
    for (int i = 0; i < typeNames.length; i++) {
      for (String suffix : FIELD_SUFFIXES) {
        doWhiteboxCreateFields(
            "whitebox_p_" + typeNames[i] + suffix, expectedClasses[i], values[i]);
        typesTested.add("*_p_" + typeNames[i] + suffix);
      }
    }
    Set<String> typesToTest = new HashSet<>();
    for (DynamicField dynField : h.getCore().getLatestSchema().getDynamicFields()) {
      if (dynField.getPrototype().getType() instanceof PointField) {
        typesToTest.add(dynField.getRegex());
      }
    }
    assertEquals("Missing types in the test", typesTested, typesToTest);
  }

  /**
   * Calls {@link #callAndCheckCreateFields} on each of the specified values. This is a convenience
   * method for testing the same fieldname with multiple inputs.
   *
   * @see #callAndCheckCreateFields
   */
  private void doWhiteboxCreateFields(
      final String fieldName, final Class<?> pointType, final Object... values) throws Exception {

    for (Object value : values) {
      // ideally we should require that all input values be diff forms of the same logical value (ie
      // '"42"' vs 'new Integer(42)') and assert that each produces an equivalent list of
      // IndexableField objects but that doesn't seem to work -- appears not all IndexableField
      // classes override Object.equals?
      final List<IndexableField> result = callAndCheckCreateFields(fieldName, pointType, value);
      assertNotNull(value + " => null", result);
    }
  }

  /**
   * Calls {@link SchemaField#createFields} on the specified value for the specified field name, and
   * asserts that the results match the SchemaField properties, with an additional check that the
   * <code>pointType</code> is included if and only if the SchemaField is "indexed"
   */
  private List<IndexableField> callAndCheckCreateFields(
      final String fieldName, final Class<?> pointType, final Object value) {
    final SchemaField sf = h.getCore().getLatestSchema().getField(fieldName);
    final List<IndexableField> results = sf.createFields(value);
    final Set<IndexableField> resultSet = new LinkedHashSet<>(results);
    assertEquals("duplicates found in results? " + results, results.size(), resultSet.size());

    final Set<Class<?>> resultClasses = new HashSet<>();
    for (IndexableField f : results) {
      resultClasses.add(f.getClass());

      if (!sf.hasDocValues()) {
        assertFalse(
            f.toString(),
            (f instanceof NumericDocValuesField) || (f instanceof SortedNumericDocValuesField));
      }
    }
    assertEquals(
        fieldName + " stored? Result Fields: " + Arrays.toString(results.toArray()),
        sf.stored(),
        resultClasses.contains(StoredField.class));
    assertEquals(
        fieldName + " indexed? Result Fields: " + Arrays.toString(results.toArray()),
        sf.indexed(),
        resultClasses.contains(pointType));
    if (sf.multiValued()) {
      assertEquals(
          fieldName + " docvalues? Result Fields: " + Arrays.toString(results.toArray()),
          sf.hasDocValues(),
          resultClasses.contains(SortedNumericDocValuesField.class));
    } else {
      assertEquals(
          fieldName + " docvalues? Result Fields: " + Arrays.toString(results.toArray()),
          sf.hasDocValues(),
          resultClasses.contains(NumericDocValuesField.class));
    }

    return results;
  }
}
